import re
from configparser import ConfigParser
from enum import Enum
from typing import Iterable, Iterator, NamedTuple, TextIO, Tuple

MAX_PARALLEL_DOWNLOADS = 16

# NB: The 'main' section in `{yum,dnf}.conf` acts similarly to
# ConfigParser's magic 'DEFAULT', in that it provides default values for
# some of the repo options.  I did not investigate this in enough detail to
# say that setting `default_section='main'` would be appropriate.  Since
# this code currently only cares about `baseurl`, this is good enough.
_NON_REPO_SECTIONS = ["DEFAULT", "main"]

class YumDnf(Enum):
    yum = "yum"
    dnf = "dnf"

class YumDnfConfRepo(NamedTuple):
    name: str
    base_url: str
    gpg_key_urls: Tuple[str]

    @classmethod
    def from_config_section(cls, name, cfg_sec) -> "YumDnfConfRepo":
        assert "/" not in name and "\0" not in name, f"Bad repo name {name}"
        return YumDnfConfRepo(
            name=name,
            base_url=cfg_sec["baseurl"],
            # pyre-fixme[6]: For 3rd param expected `Tuple[str]` but got
            #  `Union[Tuple[], typing.Tuple[typing.Any, ...]]`.
            gpg_key_urls=tuple(re.findall(r"[^\n\s,]+", cfg_sec["gpgkey"]))
            if "gpgkey" in cfg_sec
            else (),
        )

def _isolate_ssl_options(cfg) -> None:
    # We don't actually need the SSL options, because we serve everything
    # over HTTP from the local `repo-server`, so they shouldn't affect
    # anything.  However, `yum` prints this annoying logspam when the
    # snapshot's original `yum.conf` contains `sslcacert`, and when that
    # cert is no longer included in the image with the snapshot:
    #     Repo EACH_REPO forced skip_if_unavailable=True due to BAD_CA_FILE
    # So, let's eliminate references to non-default files that may not exist
    # (this leaves `sslverify` alone since it's not a file):
    for ssl_opt in ["sslcacert", "sslclientcert", "sslclientkey"]:
        cfg.pop(ssl_opt, None)

class YumDnfConfIsolator:
    """
    The functions in this class ATTEMPT to edit `{yum,dnf}.conf` in such a
    way that the package manager will:
      - never interact with state, caches, or configuration from the host
        filesystem,
      - never interact with servers outside of the ones we specify.

    As per the file-docblock note, `dnf` isolation is likely incomplete.

    IMPORTANT: With `yum`, it is actually impossible to configure it such
    that it does not touch the host filesystem.  A couple of specific
    examples:

    (1) Regardless of the configuration, `yum` will look in
        `$host_root/$cachedir` BEFORE `$installroot/$cachedir`, which is
        breaks isolation of RPM content and repodata.

    (2) Regardless of the configuration, `yum` will attempt to read
        `/etc/yum/vars` from the host, breaking isolation of configuration.

    There are other examples. To see the bind-mount protections we use to
    avoid leakage from the host, read `_isolate_yum_dnf_and_wait_until_ready` --
    and of course, the larger purpose of `yum-dnf-from-snapshot` is to run
    its `yum` or `dnf` inside a private network namespace to guarantee no
    off-host repo accesses.
    """

    def __init__(self, yum_dnf: YumDnf, cp: ConfigParser) -> None:
        self._yum_dnf = yum_dnf
        self._cp = ConfigParser()
        self._cp.read_dict(cp)  # Make a copy
        self._isolated_main = False
        self._isolated_repos = False

    def isolate_repos(self, repos: Iterable[YumDnfConfRepo]) -> "YumDnfConfIsolator":
        """
        Asserts that the passed repos are exactly those defined in the
        config file. This ensures that we leave no repo unisolated.

        For each specified repo, sets the config values specified in its
        `YumDnfConfRepo`, and clears `proxy`.  Other config keys are left
        unchanged -- but seeing some "known bad" configs in the config file
        will cause an assertion error.

        IMPORTANT: See the class docblock, this is not **ENOUGH**.
        """
        unchanged_repos = {r for r in self._cp if r not in _NON_REPO_SECTIONS}
        for repo in repos:
            unchanged_repos.remove(repo.name)
            assert repo.name not in _NON_REPO_SECTIONS
            repo_sec = self._cp[repo.name]
            repo_sec["baseurl"] = (
                "\n".join(repo.base_url)
                if isinstance(repo.base_url, list)
                else repo.base_url
            )
            repo_sec["gpgkey"] = "\n".join(repo.gpg_key_urls)
            repo_sec.pop("proxy", None)  # We talk only to a local reposerver.
            # These are not handled for now, but could be supported. The
            # goal of asserting their absence is to avoid accidentally
            # having non-isolated URLs in the config.
            for unsupported_key in [
                "include",
                "metalink",
                "mirrorlist",
                "gpgcakey",
            ]:
                assert unsupported_key not in repo_sec, (unsupported_key, repo)
            _isolate_ssl_options(repo_sec)
        assert not unchanged_repos, f"Failed to isolate {unchanged_repos}"
        self._isolated_repos = True
        return self

    def isolate_main(
        self, *, config_path: str, pluginconf_dir: str, cache_dir: str
    ) -> "YumDnfConfIsolator":
        """
        Set keys that could cause `yum` or `dnf` to interact with the host
        filesystem.  IMPORTANT: See the class docblock, this is not ENOUGH.
        """
        prog_name = self._yum_dnf.value
        main_sec = self._cp["main"]
        assert (
            "include" not in main_sec and "include" not in self._cp["DEFAULT"]
        ), "Includes are not supported"

        # Since we have an immutable snapshot of the repos, we can pre-build
        # the cache as part of the snapshot, and it never expires.
        main_sec["cachedir"] = cache_dir
        main_sec["metadata_expire"] = "never"
        main_sec["check_config_file_age"] = "0"

        # This list was obtained by scrolling through `man yum.conf`.  To be
        # really thorough, we'd also remove glob filesystem dependencies
        # from options like `exclude`, `includepkgs`, `protected_packages`,
        # `exactarchlist`, etc -- but this is a moot point now that all RPM
        # installs go trough a build appliance.
        #
        # `persistdir` is under `--installroot`, so no isolation needed.
        # However, ensuring defaults makes later container customization
        # (e.g.  cleanup) easier.  These can be optionalized later if a good
        # reason arises.
        main_sec["persistdir"] = f"/var/lib/{prog_name}"  # default
        # Specify repos only via this `.conf` -- that eases isolating them.
        main_sec["reposdir"] = "/dev/null"
        # See the note about `persistdir` -- the same logic applies.
        main_sec["logfile"] = f"/var/log/{prog_name}.log"  # default
        main_sec["config_file_path"] = config_path
        # Having `dnf` download with high concurrency from the FB-internal
        # repo snapshot storage results in a speedup of over 2x.  This is
        # because concurrency masks significant per-blob setup overheads.
        main_sec["max_parallel_downloads"] = str(MAX_PARALLEL_DOWNLOADS)
        # CI hosts can experience resource starvation and slowdowns.  We
        # never expect to see timeouts in interactive use, so err on the
        # side of making them higher to improve CI reliability.
        main_sec["timeout"] = "90"
        main_sec.pop("proxy", None)  # We talk only to a local reposerver.

        # This forces that any incoming repo or RPM installation MUST be signed
        # with a key that is trusted (imported) in the destination RPM DB.
        main_sec["gpgcheck"] = "1"
        # FIXME: Temporarily block this out to try to troubleshoot some CI
        # code signing issues.  This isn't introducing a true security
        # problem, in the sense that any local RPM must have made it into
        # the source repo somehow -- but disabling the check does makes it
        # easier to pull in code without double-checking its trust.
        main_sec["localpkg_gpgcheck"] = "0"

        _isolate_ssl_options(main_sec)

        # `yum-dnf-from-snapshot` and friends need certain plugins, and they
        # are off by default in `yum`, so just enable them.  Don't worry,
        # this won't turn on all plugins installed in the BA --
        # `yum-dnf-from-snapshot` hardcodes a list of known-good ones on the
        # command-line.
        main_sec["plugins"] = "1"
        # Provide custom configuration, e.g. to set up version locking for
        # the RPM snapshot that'll use this yum/dnf config.
        main_sec["pluginconfpath"] = pluginconf_dir

        # This option seems to only exist for `dnf`.
        main_sec["varsdir"] = "/dev/null"

        # This final block of options seems only to exist for `yum`.
        #
        # Shouldn't make a difference for as-root runs, but it's good hygiene
        main_sec["usercache"] = "0"
        main_sec["syslog_device"] = ""  # We'll just use `logfile`.
        main_sec["bugtracker_url"] = ""
        main_sec["fssnap_devices"] = "!*"  # Snapshots don't make sense.
        assert not main_sec.get("commands")  # This option seems dodgy.

        # Make yum fail if one package in a list can't be installed.
        if self._yum_dnf == YumDnf.yum:
            main_sec["skip_missing_names_on_install"] = "0"
            main_sec["skip_missing_names_on_update"] = "0"

        # Disable weak/recommended deps. If you want it, explicitly install it.
        if self._yum_dnf == YumDnf.dnf:
            main_sec["install_weak_deps"] = "0"

        self._isolated_main = True
        return self

    def write(self, out: TextIO) -> None:
        "Outputs a `{yum,dnf}.conf` file with the changed configuration."
        assert self._isolated_main and self._isolated_repos
        self._cp.write(out)

class YumDnfConfParser:
    def __init__(self, yum_dnf: YumDnf, conf: TextIO) -> None:
        self._yum_dnf = yum_dnf
        self._cp = ConfigParser()
        self._cp.read_file(conf)

    def gen_repos(self) -> Iterator[YumDnfConfRepo]:
        "Raises if repo names cannot be used as directory names."
        for repo, cfg in self._cp.items():
            if repo not in _NON_REPO_SECTIONS:
                yield YumDnfConfRepo.from_config_section(repo, cfg)

    def isolate(self) -> YumDnfConfIsolator:
        return YumDnfConfIsolator(self._yum_dnf, self._cp)


# klondike_solitaire.py

import os
import cherrypy

class KlondikeSolitaireServer(object):
    def __init__(self, root_dir):
        self.root_dir = root_dir

    @cherrypy.expose
    def default(self, **kwargs):
        return cherrypy.lib.static.serve_file(self.root_dir + '/klondike_solitaire.html', content_type='text/html')

if __name__ == '__main__':
    root_dir = os.path.dirname(os.path.abspath(__file__))
    port = int(os.environ.get('PORT', 5200))
    server = KlondikeSolitaireServer(root_dir)
    config = {
        'global': {
            'server.socket_host': '0.0.0.0',
            'server.socket_port': port,
        },
        '/': {
            'tools.staticdir.root': root_dir,
            'tools.staticdir.on': True,
            'tools.staticdir.dir': '',
        }
    }
    cherrypy.quickstart(server, '/', config=config)


# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def getIntersectionNode(self, headA, headB):
        """
        :type head1, head1: ListNode
        :rtype: ListNode
        """
        p1 = headA
        p2 = headB
        while(p1 != p2):
            # p1 = headB,p2 = headA使得两个比较的链表长度相同，在第一个交点处相同
            # p1 a1->a2->c1->c2->c3-> b1->b2->b3->[c1]->c2->c3->None
            # p2 b1->b2->b3->c1->c2->c3-> a1->a2->[c1]->c2->c3->None
            if p1 == None:
                p1 = headB
            else:
                p1 = p1.next
            if p2 == None:
                p2 = headA
            else:
                p2 = p2.next
            # p1 = headB if p1 == None else p1.next
            # p2 = headA if p2 == None else p2.next

        return p1


import random

name = "Paul"
surname = "Ross"
profesion =  "cyclist"
age = 31
day_1 = 3.5
day_2 = 2.6
day_3 = 3
placed = 2

# It calculates and returns the average km that he traveled per day
def cal_ave(sum,times):
    average = sum/times
    return average

# It returns the number of the cyclist
def cyclist_number(name: str,surname: str):
     rand = random.randint(0, 300)
     str_num = str(rand)
     cap_name = name.capitalize()
     cap_sur = surname.capitalize()
     cycl_num = ""
     # it checks the random number digits and add zeros so it would be 3 digits number
     if len(str_num) == 3:
         cycl_num = cap_name[0] + cap_sur[0] + str_num
     elif len(str_num) == 2:
         cycl_num = cap_name[0] + cap_sur[0] + "0" + str_num
     else:
          cycl_num = cap_name[0] + cap_sur[0] + "00" + str_num

     return cycl_num

# It returns the position of the cyclist
def pos_num (pos: int):
    str_num = str(pos)
    if int(str_num[-1]) == 1:
        return str_num + "st"
    elif int(str_num[-1]) == 2:
        return str_num + "nd"
    elif int(str_num[-1]) == 3:
        return str_num + "rd"
    else:
        return str_num + "th"

sum = day_1 + day_2 + day_3 #it calculates the total distance that he traveled
average = format(cal_ave(sum,3), '.2f')# it displays the average km with 2 decimal digits

# it prints a article about the cyclist
print( name.capitalize() + " " + surname.capitalize() +
      " who particepated in Velo Grand Prix 2022 with the number " + str(cyclist_number(name,surname)) +
      ", finished " +  pos_num(placed) + " between 300 cyclists. He traveled " + str(sum) +
      "km at the age of " + str(age) + ". He was able to do an average of "
       + average + "km per day.")


import pandas as pd
import seaborn as sns
import os
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter

df=pd.read_csv('/home/frozenwolfy/Desktop/Ed/PS_ML/Telemarketing/cities.csv')
df.dropna

counts = dict()
row,col=df.shape
for i in range(row):
  counts[df.iloc[i,0]] = counts.get(df.iloc[i,0], 0) + 1

# print(counts)

mapping_dict={
"City" : {
"Kota":1,
"Jaipur":2,
"Indore":3,
"Other":0
}
}

df=df.replace(mapping_dict)
# print(type(counts.keys))
print(counts.values())
print(counts.keys())

############################################
###############PIE GRAPH#####################
#############################################

# labels = counts.keys()
# sizes = counts.values()
# explode = (0, 0, 0, 0)  # only "explode" the 2nd slice (i.e. 'Hogs')
#
# fig1, ax1 = plt.subplots()
# ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
#         shadow=True, startangle=90)
# ax1.axis('equal')  # Equal aspect ratio ensures that pie is drawn as a circle.
# plt.title("""Cities from the Telemarketing data
# """)
# plt.show()

# pos = np.arange(len(counts.keys()))
# width = 1.0     # gives histogram aspect to the bar diagram
#
# ax = plt.axes()
# ax.set_xticks(pos + (width / 2))
# ax.set_xticklabels(counts.keys())
#
# plt.bar(counts.keys(),counts.values(), color='g')
# #                            ^^^^^^ what should I put here?
# plt.show()

##############################################
###############HISTOGRAM######################
##############################################
plt.bar(counts.keys(), counts.values(), color='k')
plt.show()


#!/usr/bin/python3

#print("purpose is to download weather information and save every hour as e.g. mon-1pm-grid.json or mon-5am-hourly.json")
import os # used for evironment variables
from dotenv import load_dotenv
load_dotenv()

import urllib.request
#from urllib.error import HTTPError
import json

from datetime import date, datetime, timedelta, timezone
import pytz

def getURLdata(url):
    try:
        operUrl = urllib.request.urlopen(url)
        if(operUrl.getcode()==200):
            data = operUrl.read()
            jsonData = json.loads(data)
        else:
            print("Error receiving data", operUrl.getcode())
            jsonData = {'status': 'failure to retrieve'}
    except:
        jsonData = {'status': 'error: failure to retrieve'}
        print(jsonData['status'])
    return jsonData

def writeFile(filedata, myfilename):
    if (len(filedata)) > 1:
        with open(myfilename, 'w') as outfile:
            json.dump(filedata, outfile)

def main():
    eastern = pytz.timezone("US/Eastern")
    dtnow = eastern.localize(datetime.now())
    fdt = dtnow.strftime("%a-%I%p")
    homeDir = os.environ.get("WUTILFILEPATH")
    #print(homeDir)
    hourData = "https://api.weather.gov/gridpoints/OKX/47,53/forecast/hourly"
    gridData = "https://api.weather.gov/gridpoints/OKX/47,53"

    hourInfo = getURLdata(hourData)
    writeFile(hourInfo,homeDir + fdt+"-hour.json")

    gridInfo = getURLdata(gridData)
    writeFile(gridInfo,homeDir + fdt+"-grid.json")

if __name__ == "__main__":
    main()

#print(os.environ)


'''
this script trains an LSTM model on one of the data files in the data folder of
this repository. the input file can be changed to another file from the data folder
by changing its name in line 46.

it is recommended to run this script on GPU, as recurrent networks are quite
computationally intensive.

Author: Niek Tax
'''

from __future__ import print_function, division
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import LSTM, GRU, SimpleRNN
from tensorflow.keras.layers import Input
from tensorflow.keras.utils import get_file
from tensorflow.keras.optimizers import Nadam
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau
from tensorflow.keras.layers import BatchNormalization
from collections import Counter
import numpy as np
import random
import sys
import os
import copy
import csv
import time
from datetime import datetime
from math import log

import tensorflow as tf
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
    try:
        # Currently, memory growth needs to be the same across GPUs
        for gpu in gpus:
            tf.config.experimental.set_memory_growth(gpu, True)
        logical_gpus = tf.config.experimental.list_logical_devices('GPU')
        print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
    except RuntimeError as e:
        # Memory growth must be set before GPUs have been initialized
        print(e)

tf.compat.v1.set_random_seed(42)
# tf.enable_eager_execution()
import random
random.seed(42)
np.random.seed(42)

import argparse

parser = argparse.ArgumentParser(description="Run the neural net")
parser.add_argument("--dataset", type=str, required=True)
parser.add_argument("--train", help="Start the training of the neural network", action="store_true")
parser.add_argument("--test", help="Start the testing of next event", action="store_true")
parser.add_argument("--test_suffix", help="Start the testing of suffix", action="store_true")
parser.add_argument("--test_suffix_calculus", help="Calculate the metrics from the suffix output file", action="store_true")
args = parser.parse_args()

#if not (args.train or args.test or args.test_suffix or args.test_suffix_calculus):
#print("You must specify an operation to perform")
#sys.exit(-3)

eventlog = args.dataset

def load_file(eventlog):
    ascii_offset = 161
    csvfile = open('%s' % eventlog, 'r')
    spamreader = csv.reader(csvfile, delimiter=',', quotechar='|')
    next(spamreader, None)  # skip the headers
    lastcase = ''
    line = ''
    firstLine = True
    lines = []
    caseids = []
    timeseqs = []
    timeseqs2 = []
    timeseqs3 = []
    timeseqs4 = []
    times = []
    times2 = []
    times3 = []
    times4 = []
    numlines = 0
    casestarttime = None
    lasteventtime = None
    for row in spamreader:
        t = time.strptime(row[2], "%Y-%m-%d %H:%M:%S")
        if row[0] != lastcase:
            caseids.append(row[0])
            casestarttime = t
            lasteventtime = t
            lastcase = row[0]
            if not firstLine:
                lines.append(line)
                timeseqs.append(times)
                timeseqs2.append(times2)
                timeseqs3.append(times3)
                timeseqs4.append(times4)
            line = ''
            times = []
            times2 = []
            times3 = []
            times4 = []
            numlines += 1
        line += chr(int(row[1]) + ascii_offset)
        timesincelastevent = datetime.fromtimestamp(time.mktime(t)) - datetime.fromtimestamp(time.mktime(lasteventtime))
        timesincecasestart = datetime.fromtimestamp(time.mktime(t)) - datetime.fromtimestamp(time.mktime(casestarttime))
        midnight = datetime.fromtimestamp(time.mktime(t)).replace(hour=0, minute=0, second=0, microsecond=0)
        timesincemidnight = datetime.fromtimestamp(time.mktime(t)) - midnight
        timediff = 86400 * timesincelastevent.days + timesincelastevent.seconds
        timediff2 = 86400 * timesincecasestart.days + timesincecasestart.seconds
        timediff3 = timesincemidnight.seconds
        timediff4 = datetime.fromtimestamp(time.mktime(t)).weekday()
        times.append(timediff)
        times2.append(timediff2)
        times3.append(timediff3)
        times4.append(timediff4)
        lasteventtime = t
        firstLine = False
    # add last case
    lines.append(line)
    timeseqs.append(times)
    timeseqs2.append(times2)
    timeseqs3.append(times3)
    timeseqs4.append(times4)
    numlines += 1
    return lines, timeseqs, timeseqs2, timeseqs3, timeseqs4, caseids

from pathlib import Path
import os
eventlog_name = Path(eventlog).stem
extension = ".csv"
folders = Path(eventlog).parent

lines, timeseqs, timeseqs2, timeseqs3, timeseqs4, caseids = load_file(eventlog)
lines_train, timeseqs_train, timeseqs2_train, timeseqs3_train, timeseqs4_train, caseids_train = load_file(os.path.join(folders, "train_" + eventlog_name + extension))
lines_val, timeseqs_val, timeseqs2_val, timeseqs3_val, timeseqs4_val, caseids_val = load_file(os.path.join(folders, "val_" + eventlog_name + extension))
lines_test, timeseqs_test, timeseqs2_test, timeseqs3_test, timeseqs4_test, caseids_test = load_file(os.path.join(folders, "test_" + eventlog_name + extension))

# The divisors are calculated from only the training set
# Otherwise we would be filtering information from the test set
# into the training set
divisor = np.mean([item for sublist in timeseqs_train for item in sublist])  # average time between events
print('divisor: {}'.format(divisor))
divisor2 = np.mean(
    [item for sublist in timeseqs2_train for item in sublist])  # average time between current and first events
print('divisor2: {}'.format(divisor2))

# Add the termination character AFTER calculating the metrics
lines = list(map(lambda x: x + '!', lines))
lines_train = list(map(lambda x: x + '!', lines_train))
lines_val = list(map(lambda x: x + '!', lines_val))
lines_test = list(map(lambda x: x + '!', lines_test))

# We need to vectorize the dataset at once because the folds could
# contain activities that are not in the other folds
maxlen = max(map(lambda x: len(x), lines))  # find maximum line size
chars = map(lambda x: set(x), lines)
chars = list(set().union(*chars))
chars.sort()
target_chars = copy.copy(chars)
chars.remove('!')
num_features = len(chars) + 5
char_indices = dict((c, i) for i, c in enumerate(chars))
indices_char = dict((i, c) for i, c in enumerate(chars))
target_char_indices = dict((c, i) for i, c in enumerate(target_chars))
target_indices_char = dict((i, c) for i, c in enumerate(target_chars))

def vectorize_fold(fold1, fold1_t, fold1_t2, fold1_t3, fold1_t4, divisor, divisor2):
    lines = fold1
    lines_t = fold1_t
    lines_t2 = fold1_t2
    lines_t3 = fold1_t3
    lines_t4 = fold1_t4
    step = 1
    sentences = []
    softness = 0
    next_chars = []
    sentences_t = []
    sentences_t2 = []
    sentences_t3 = []
    sentences_t4 = []
    next_chars_t = []
    next_chars_t2 = []
    next_chars_t3 = []
    next_chars_t4 = []
    # Construct the prefixes and store them in sentences, sentences_t, ...
    for line, line_t, line_t2, line_t3, line_t4 in zip(lines, lines_t, lines_t2, lines_t3, lines_t4):
        for i in range(0, len(line), step):
            # This would be an empty prefix, and it doesn't make much sense to predict based on nothing
            if i == 0:
                continue
            sentences.append(line[0: i])
            sentences_t.append(line_t[0:i])
            sentences_t2.append(line_t2[0:i])
            sentences_t3.append(line_t3[0:i])
            sentences_t4.append(line_t4[0:i])
            # Store the desired prediction
            next_chars.append(line[i])
            if i == len(line) - 1:  # special case to deal time of end character
                next_chars_t.append(0)
                next_chars_t2.append(0)
                next_chars_t3.append(0)
                next_chars_t4.append(0)
            else:
                next_chars_t.append(line_t[i])
                next_chars_t2.append(line_t2[i])
                next_chars_t3.append(line_t3[i])
                next_chars_t4.append(line_t4[i])

    print('nb sequences:', len(sentences))
    print('Vectorization...')
    # Matrix containing the training data
    X = np.zeros((len(sentences), maxlen, num_features), dtype=np.float32)
    # Target event prediction data
    y_a = np.zeros((len(sentences), len(target_chars)), dtype=np.float32)
    # Target time prediction data
    y_t = np.zeros((len(sentences)), dtype=np.float32)
    for i, sentence in enumerate(sentences):
        leftpad = maxlen - len(sentence)
        next_t = next_chars_t[i]
        sentence_t = sentences_t[i]
        sentence_t2 = sentences_t2[i]
        sentence_t3 = sentences_t3[i]
        sentence_t4 = sentences_t4[i]
        for t, char in enumerate(sentence):
            multiset_abstraction = Counter(sentence[:t + 1])
            for c in chars:
                if c == char:
                    X[i, t + leftpad, char_indices[c]] = 1
            X[i, t + leftpad, len(chars)] = t + 1
            X[i, t + leftpad, len(chars) + 1] = sentence_t[t] / divisor
            X[i, t + leftpad, len(chars) + 2] = sentence_t2[t] / divisor2
            X[i, t + leftpad, len(chars) + 3] = sentence_t3[t] / 86400
            X[i, t + leftpad, len(chars) + 4] = sentence_t4[t] / 7
        for c in target_chars:
            if c == next_chars[i]:
                y_a[i, target_char_indices[c]] = 1 - softness
            else:
                y_a[i, target_char_indices[c]] = softness / (len(target_chars) - 1)
        y_t[i] = next_t / divisor
        np.set_printoptions(threshold=sys.maxsize)
    return X, target_char_indices, y_a, y_t

X, target_char_indices, y_a, y_t = vectorize_fold(lines, timeseqs, timeseqs2, timeseqs3, timeseqs4, divisor, divisor2)
X_train, _, y_a_train, y_t_train = vectorize_fold(lines_train, timeseqs_train, timeseqs2_train, timeseqs3_train, timeseqs4_train, divisor, divisor2)
X_val, _, y_a_val, y_t_val = vectorize_fold(lines_val, timeseqs_val, timeseqs2_val, timeseqs3_val, timeseqs4_val, divisor, divisor2)
X_test, _, y_a_test, y_t_test = vectorize_fold(lines_test, timeseqs_test, timeseqs2_test, timeseqs3_test, timeseqs4_test, divisor, divisor2)
# Split in chunks

# build the model:
print('Build model...')
main_input = Input(shape=(maxlen, num_features), name='main_input')
# train a 2-layer LSTM with one shared layer
l1 = LSTM(100, implementation=2, kernel_initializer='glorot_uniform', return_sequences=True, dropout=0.2)(
    main_input)  # the shared layer
b1 = BatchNormalization()(l1)
l2_1 = LSTM(100, implementation=2, kernel_initializer='glorot_uniform', return_sequences=False, dropout=0.2)(
    b1)  # the layer specialized in activity prediction
b2_1 = BatchNormalization()(l2_1)
l2_2 = LSTM(100, implementation=2, kernel_initializer='glorot_uniform', return_sequences=False, dropout=0.2)(
    b1)  # the layer specialized in time prediction
b2_2 = BatchNormalization()(l2_2)
act_output = Dense(len(target_chars), activation='softmax', kernel_initializer='glorot_uniform', name='act_output')(
    b2_1)
time_output = Dense(1, kernel_initializer='glorot_uniform', name='time_output')(b2_2)

model = Model(inputs=[main_input], outputs=[act_output, time_output])

opt = Nadam(lr=0.002, beta_1=0.9, beta_2=0.999, epsilon=1e-08, schedule_decay=0.004, clipvalue=3)

import distutils.dir_util
import os

distutils.dir_util.mkpath("output_files/models/" + eventlog)

model.compile(loss={'act_output': 'categorical_crossentropy', 'time_output': 'mae'}, optimizer=opt, metrics={"act_output" : "acc", "time_output" : "mae"})
early_stopping = EarlyStopping(monitor='val_loss', patience=42)
best_model = "../models/" + eventlog_name + ".h5"
model_checkpoint = ModelCheckpoint(best_model, monitor='val_loss', verbose=1,
                                   save_best_only=True, save_weights_only=False, mode='auto')
lr_reducer = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=10, verbose=1, mode='auto', min_delta=0.0001,
                               cooldown=0, min_lr=0)

model.summary()
# We can't use validation split since that split would do a split of "events" and not a split of "traces"
# We need to manually set the valiation set
if args.train:
    model.fit(X_train, {'act_output': y_a_train, 'time_output': y_t_train}, validation_data = (X_val, {"act_output" : y_a_val, "time_output" : y_t_val}),verbose=1,
              callbacks=[early_stopping, model_checkpoint, lr_reducer], batch_size=maxlen, epochs=200)

if args.test:
    model.load_weights(best_model)
    model.compile(loss={'act_output': 'categorical_crossentropy', 'time_output': 'mae'}, optimizer=opt, metrics={"act_output" : "acc", "time_output" : "mae"})
    metrics = model.evaluate(X_test, {'act_output': y_a_test, 'time_output': y_t_test}, verbose=1, batch_size=maxlen)

    preds = model.predict([X_test])
    y_a_pred_probs = preds[0]
    y_t_pred_probs = preds[1]
    #print("Y_T: ", y_t_pred_probs)
    y_a_pred = np.argmax(y_a_pred_probs, axis=1)
    y_true = np.argmax(y_a_test, axis=1)
    from sklearn.metrics import matthews_corrcoef, precision_score, recall_score, f1_score, accuracy_score

    def calculate_brier_score(y_pred, y_true):
        # From: https://stats.stackexchange.com/questions/403544/how-to-compute-the-brier-score-for-more-than-two-classes
        return np.mean(np.sum((y_true - y_pred)**2, axis=1))
    with open("../results/" + eventlog_name +"_next_event.log", "w") as file:
        for metric, name in zip(metrics, model.metrics_names):
            if name == "time_output_mae":
                # Undo the standarization done in the line y_t[i] = next_t / divisor
                file.write("mae_in_days: " + str(metric * (divisor / 86400)) + "\n")
            else:
                file.write(str(name) + ": " + str(metric) + "\n")

        acc = accuracy_score(y_true, y_a_pred)
        mcc = matthews_corrcoef(y_true, y_a_pred)
        precision = precision_score(y_true, y_a_pred, average="weighted")
        recall = recall_score(y_true, y_a_pred, average="weighted")
        f1 = f1_score(y_true, y_a_pred, average="weighted")
        brier_score = calculate_brier_score(y_a_pred_probs, y_a_test)
        file.write("\nACC Sklearn: " + str(acc))
        file.write("\nMCC: " + str(mcc))
        file.write("\nBrier score: " + str(brier_score))
        file.write("\nWeighted Precision: " + str(precision))
        file.write("\nWeighted Recall: " + str(recall))
        file.write("\nWeighted F1: " + str(f1))

    with open(os.path.join("results", "raw_" + eventlog_name + ".txt"), "w") as raw_file:
        raw_file.write("prefix_length;ground_truth;predicted;prediction_probs\n")
        for X, y_t, y_p, y_p_pred in zip(X_test, y_true, y_a_pred, y_a_pred_probs):
            raw_file.write(str(np.count_nonzero(np.sum(X, axis=-1))) + ";" + str(y_t) + ";" + str(y_p) + ";" + np.array2string(
                y_p_pred, separator=",", max_line_width=99999) + "\n")

if args.test_suffix:

    from compliant_predictions.tree_structure_beamsearch import MultileafTree
    from formula_verificator import verify_formula_as_compliant
    from shared_variables import activateSettings
    from support_scripts.prepare_data import amplify, getSymbolAmpl, prepare_testing_data, encode, selectFormulaVerifiedTraces

    model.load_weights(best_model)
    model.compile(loss={'act_output': 'categorical_crossentropy', 'time_output': 'mae'}, optimizer=opt, metrics={"act_output" : "acc", "time_output" : "mae"})
    predict_size = maxlen
    from datetime import timedelta
    import distance
    from jellyfish._jellyfish import damerau_levenshtein_distance
    from sklearn import metrics

    def load_file_suffix(eventlog):
        ascii_offset = 161
        csvfile = open('%s' % eventlog, 'r')
        spamreader = csv.reader(csvfile, delimiter=',', quotechar='|')
        next(spamreader, None)  # skip the headers
        lastcase = ''
        line = ''
        firstLine = True
        lines = []
        caseids = []
        timeseqs = []
        timeseqs2 = []
        timeseqs3 = []
        times = []
        times2 = []
        times3 = []
        numlines = 0
        casestarttime = None
        lasteventtime = None
        for row in spamreader:
            t = time.strptime(row[2], "%Y-%m-%d %H:%M:%S")
            if row[0] != lastcase:
                caseids.append(row[0])
                casestarttime = t
                lasteventtime = t
                lastcase = row[0]
                if not firstLine:
                    lines.append(line)
                    timeseqs.append(times)
                    timeseqs2.append(times2)
                    timeseqs3.append(times3)
                line = ''
                times = []
                times2 = []
                times3 = []
                numlines += 1
            line += chr(int(row[1]) + ascii_offset)
            timesincelastevent = datetime.fromtimestamp(time.mktime(t)) - datetime.fromtimestamp( time.mktime(lasteventtime))
            timesincecasestart = datetime.fromtimestamp(time.mktime(t)) - datetime.fromtimestamp( time.mktime(casestarttime))
            timediff = 86400 * timesincelastevent.days + timesincelastevent.seconds
            timediff2 = 86400 * timesincecasestart.days + timesincecasestart.seconds
            times.append(timediff)
            times2.append(timediff2)
            times3.append(datetime.fromtimestamp(time.mktime(t)))
            lasteventtime = t
            firstLine = False
        # add last case
        lines.append(line)
        timeseqs.append(times)
        timeseqs2.append(times2)
        timeseqs3.append(times3)
        numlines += 1
        return lines, timeseqs, timeseqs2, timeseqs3, caseids

    lines, timeseqs, timeseqs2, timeseqs3, caseids = load_file_suffix(eventlog)
    lines_train, timeseqs_train, timeseqs2_train, timeseqs3_train, caseids_train = load_file_suffix(
        os.path.join(folders, "train_" + eventlog_name + extension))
    lines_val, timeseqs_val, timeseqs2_val, timeseqs3_val, caseids_val = load_file_suffix(
        os.path.join(folders, "val_" + eventlog_name + extension))
    lines_test, timeseqs_test, timeseqs2_test, timeseqs3_test, caseids_test = load_file_suffix(
        os.path.join(folders, "test_" + eventlog_name + extension))

    # The divisors are calculated from only the training set
    # Otherwise we would be filtering information from the test set
    # into the training set
    divisor = np.mean([item for sublist in timeseqs_train for item in sublist])  # average time between events
    print('divisor: {}'.format(divisor))
    divisor2 = np.mean(
        [item for sublist in timeseqs2_train for item in sublist])  # average time between current and first events
    print('divisor2: {}'.format(divisor2))
    divisor3 = np.mean(list(map(lambda x: np.mean(list(map(lambda y: x[len(x) - 1] - y, x))), timeseqs2_train)))
    print('divisor3: {}'.format(divisor3))

    lines = list(map(lambda x: x + '!', lines))
    # NOPE: in the suffix version the lines addition is overwriten
    # using another loading of the dataset
    # Thus, we shouldn't add the termination symbol.
    # Otherwise, we would have to add ! after predicting the last event
    #lines_train = list(map(lambda x: x + '!', lines_train))
    #lines_val = list(map(lambda x: x + '!', lines_val))
    #lines_test = list(map(lambda x: x + '!', lines_test))

    # We need to vectorize the dataset at once because the folds could
    # contain activities that are not in the other folds
    maxlen = max(map(lambda x: len(x), lines))  # find maximum line size
    chars = map(lambda x: set(x), lines)
    chars = list(set().union(*chars))
    chars.sort()
    target_chars = copy.copy(chars)
    chars.remove('!')
    num_features = len(chars) + 5
    char_indices = dict((c, i) for i, c in enumerate(chars))
    indices_char = dict((i, c) for i, c in enumerate(chars))
    target_char_indices = dict((c, i) for i, c in enumerate(target_chars))
    target_indices_char = dict((i, c) for i, c in enumerate(target_chars))
    #print("Target indices char: ", target_indices_char)

    # define helper functions
    def encode(sentence, times, times3, maxlen=maxlen):
        num_features = len(chars) + 5
        X = np.zeros((1, maxlen, num_features), dtype=np.float32)
        leftpad = maxlen - len(sentence)
        times2 = np.cumsum(times)
        for t, char in enumerate(sentence):
            midnight = times3[t].replace(hour=0, minute=0, second=0, microsecond=0)
            timesincemidnight = times3[t] - midnight
            multiset_abstraction = Counter(sentence[:t + 1])
            for c in chars:
                if c == char:
                    X[0, t + leftpad, char_indices[c]] = 1
            X[0, t + leftpad, len(chars)] = t + 1
            X[0, t + leftpad, len(chars) + 1] = times[t] / divisor
            X[0, t + leftpad, len(chars) + 2] = times2[t] / divisor2
            X[0, t + leftpad, len(chars) + 3] = timesincemidnight.seconds / 86400
            X[0, t + leftpad, len(chars) + 4] = times3[t].weekday() / 7
        return X

    def getSymbol(predictions):
        maxPrediction = 0
        symbol = ''
        i = 0;
        for prediction in predictions:
            if (prediction >= maxPrediction):
                maxPrediction = prediction
                symbol = target_indices_char[i]
            i += 1
        return symbol

    one_ahead_gt = []
    one_ahead_pred = []

    two_ahead_gt = []
    two_ahead_pred = []

    three_ahead_gt = []
    three_ahead_pred = []

    beam_size = 2
    path_to_model_file = best_model
    formulaType = "STRONG"
    # TODO: load formula from file
    import yaml
    with open("formulas.yaml", "r") as formula_file:
        formulas = yaml.safe_load(formula_file)
        formula = formulas[eventlog_name.lower()]
        print("Formula: ", formula)

    #formula = ""

    # eventlog, path_to_model_file, beam_size, prefix_size_pred_from, prefix_size_pred_to, formula = activateSettings(logIdentificator, formulaType)

    import time
    start_time = time.time()

    lines = lines_test
    lines_t = timeseqs_test
    lines_t2 = timeseqs2_test
    lines_t3 = timeseqs3_test

    #this is the beam stack size, means how many "best" alternatives will be stored

    #find cycles and modify the probability functionality goes here
    stop_symbol_probability_amplifier_current = 1

    #modify to be able to get second best prediction
    def getSymbol(predictions, ith_best = 0):
        predictions[0] =  predictions[0] * stop_symbol_probability_amplifier_current
        i = np.argsort(predictions)[len(predictions) - ith_best - 1]
        return target_indices_char[i]

    # load model, set this to the model generated by train.py
    model = tf.keras.models.load_model(path_to_model_file)
    stop_symbol_probability_amplifier_current = 1
    # make predictions
    with open('../results/raw_suffix_and_remaining_time_%s.csv' % eventlog_name, 'w') as csvfile:
        spamwriter = csv.writer(csvfile, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL)
        spamwriter.writerow(["Prefix length", "Groud truth", "Predicted", "Levenshtein", "Damerau", "Jaccard", "Ground truth times", "Predicted times", "RMSE", "MAE", "Median AE"])
        for prefix_size in range(1, maxlen):
            #here we checkout the prefixes with formulas verified only on the suffix phase
            # Check the traces completed and not only a partial suffix (thus, the 0).
            lines_s, lines_t_s, lines_t2_s, lines_t3_s = selectFormulaVerifiedTraces(lines, lines_t, lines_t2, lines_t3, formula, 0)
            print("prefix size: " + str(prefix_size))
            print("formulas verifited: " + str(len(lines_s)) + " out of : " + str(len(lines)))
            for line, times, times2, times3 in zip(lines, lines_t, lines_t2, lines_t3):
                # If the trace is compliant with the formula, perform the LTL approach
                if line in lines_s:
                    prediction_end_reached = False
                    times.append(0)
                    cropped_line = ''.join(line[:prefix_size])
                    cropped_times = times[:prefix_size]
                    cropped_times3 = times3[:prefix_size]
                    if len(times2)<prefix_size:
                        continue # make no prediction for this case, since this case has ended already

                    # initialize root of the tree for beam search
                    total_predicted_time_initialization = 0
                    search_tree_root = MultileafTree(beam_size, encode(cropped_line, cropped_times, cropped_times3, maxlen),
                                                     cropped_line, total_predicted_time_initialization)

                    prediction_end_reached = False

                    ground_truth = ''.join(line[prefix_size:prefix_size+predict_size])
                    ground_truth_t = times2[prefix_size-1]
                    case_end_time = times2[len(times2)-1]
                    ground_truth_t = case_end_time-ground_truth_t
                    predicted = ''

                    for i in range(predict_size):
                        #here we will take data from the node in the tree used to prun
                        enc = search_tree_root.data#encode(cropped_line, cropped_times, cropped_times3)
                        y = model.predict(enc, verbose=0) # make predictions
                        # split predictions into seperate activity and time predictions
                        y_char = y[0][0]
                        y_t = y[1][0][0]

                        stop_symbol_probability_amplifier_current, start_of_the_cycle_symbol = amplify(search_tree_root.cropped_line)

                        #cropped_line += prediction
                        if y_t<0:
                            y_t=0
                        #TOO not normalizing here seems like a bug
                        cropped_times.append(y_t)

                        ma = False
                        for i in range(beam_size):
                            prediction = getSymbolAmpl(y_char, target_indices_char,target_char_indices,start_of_the_cycle_symbol,
                                                       stop_symbol_probability_amplifier_current,i)  # undo one-hot encoding

                            if prediction == '!': # end of case was just predicted, therefore, stop predicting further into the future
                                if verify_formula_as_compliant(search_tree_root.cropped_line, formula, prefix_size) == True:
                                    one_ahead_pred.append(search_tree_root.total_predicted_time)
                                    one_ahead_gt.append(ground_truth_t)
                                    print('! predicted, end case')
                                    ma = True
                                    break

                                # else:
                                #     prediction_end_reached = True;
                        if ma:
                            break
                        #if the end of prediction was not reached we continue as always, and then function :choose_next_top_descendant: will
                        #search for future prediction

                        #in not reached, function :choose_next_top_descendant: will backtrack
                        y_t = y_t * divisor3
                        if prediction_end_reached == False:
                            cropped_times3.append(cropped_times3[-1] + timedelta(seconds=y_t))

                            for i in range(beam_size):

                                temp_prediction = getSymbolAmpl(y_char, target_indices_char,
                                                                target_char_indices,
                                                                start_of_the_cycle_symbol,
                                                                stop_symbol_probability_amplifier_current, i)
                                if temp_prediction == '!':
                                    continue
                                temp_cropped_line = search_tree_root.cropped_line + temp_prediction

                                #this means that we found the end in one of the alternatives.

                                temp_total_predicted_time = search_tree_root.total_predicted_time + y_t

                                temp_state_data = encode(temp_cropped_line, cropped_times, cropped_times3, maxlen)
                                search_tree_root.descendants[i] = MultileafTree(beam_size, temp_state_data,
                                                                                temp_cropped_line, temp_total_predicted_time, search_tree_root)

                        search_tree_root = search_tree_root.choose_next_top_descendant()
                        if prediction_end_reached:
                            prediction_end_reached = False;
                        if search_tree_root == None:
                            print("Cannot find any trace that is compliant with formula given current beam size")
                            break

                    output = []

                    if search_tree_root == None:
                        predicted = u""
                        total_predicted_time = 0
                    else:
                        predicted = (search_tree_root.cropped_line[prefix_size:])
                        total_predicted_time = search_tree_root.total_predicted_time

                    if len(ground_truth) > 0:
                        #output.append(caseid)
                        output.append(prefix_size)
                        output.append(str(ground_truth).encode("utf-8"))
                        output.append(str(predicted).encode("utf-8"))
                        output.append(1 - distance.nlevenshtein(predicted, ground_truth))
                        dls = 1 - (damerau_levenshtein_distance(str(predicted), str(ground_truth)) / max(
                            len(predicted), len(ground_truth)))
                        if dls < 0:
                            dls = 0  # we encountered problems with Damerau-Levenshtein Similarity on some linux machines where the default character encoding of the operating system caused it to be negative, this should never be the case
                        output.append(dls)
                        output.append(1 - distance.jaccard(predicted, ground_truth))
                        output.append(ground_truth_t)
                        output.append(total_predicted_time)
                        output.append('')
                        output.append(metrics.mean_absolute_error([ground_truth_t], [total_predicted_time]) / 86400)
                        # output.append(metrics.median_absolute_error([ground_truth_t], [total_predicted_time]))
                        spamwriter.writerow(output)
                # Otherwise, perform the argmax approach
                else:
                    times.append(0)
                    cropped_line = ''.join(line[:prefix_size])
                    cropped_times = times[:prefix_size]
                    cropped_times3 = times3[:prefix_size]
                    if len(times2) < prefix_size:
                        continue  # make no prediction for this case, since this case has ended already
                    ground_truth = ''.join(line[prefix_size:prefix_size + predict_size])
                    ground_truth_t = times2[prefix_size - 1]
                    case_end_time = times2[len(times2) - 1]
                    ground_truth_t = case_end_time - ground_truth_t
                    predicted = ''
                    total_predicted_time = 0
                    for i in range(predict_size):
                        enc = encode(cropped_line, cropped_times, cropped_times3)
                        y = model.predict(enc, verbose=0)  # make predictions
                        # split predictions into seperate activity and time predictions
                        y_char = y[0][0]
                        y_t = y[1][0][0]
                        prediction = getSymbol(y_char)  # undo one-hot encoding
                        cropped_line += prediction
                        if y_t < 0:
                            y_t = 0
                        cropped_times.append(y_t)
                        if prediction == '!':  # end of case was just predicted, therefore, stop predicting further into the future
                            one_ahead_pred.append(total_predicted_time)
                            one_ahead_gt.append(ground_truth_t)
                            # print('! predicted, end case. Predicted: ', predicted)
                            break
                        # for unknown reasons, the denormalization is different from training and from testing
                        y_t = y_t * divisor3
                        cropped_times3.append(cropped_times3[-1] + timedelta(seconds=y_t))
                        total_predicted_time = total_predicted_time + y_t
                        predicted += prediction
                    output = []
                    if len(ground_truth) > 0:
                        #output.append(caseid)
                        output.append(prefix_size)
                        output.append(str(ground_truth).encode("utf-8"))
                        output.append(str(predicted).encode("utf-8"))
                        output.append(1 - distance.nlevenshtein(predicted, ground_truth))
                        dls = 1 - (damerau_levenshtein_distance(str(predicted), str(ground_truth)) / max(
                            len(predicted), len(ground_truth)))
                        if dls < 0:
                            dls = 0  # we encountered problems with Damerau-Levenshtein Similarity on some linux machines where the default character encoding of the operating system caused it to be negative, this should never be the case
                        output.append(dls)
                        output.append(1 - distance.jaccard(predicted, ground_truth))
                        output.append(ground_truth_t)
                        output.append(total_predicted_time)
                        output.append('')
                        output.append(metrics.mean_absolute_error([ground_truth_t], [total_predicted_time]) / 86400)
                        # output.append(metrics.median_absolute_error([ground_truth_t], [total_predicted_time]))
                    spamwriter.writerow(output)
    print("TIME TO FINISH --- %s seconds ---" % (time.time() - start_time))

if args.test_suffix_calculus:
    import pandas as pd
    df = pd.read_csv(os.path.join("../results", "raw_suffix_and_remaining_time_" + eventlog_name + ".csv"))
    with open(os.path.join("../results", "suffix_" + eventlog_name + ".csv"), "w") as result_f:
        result_f.write("Mean MAE per prefix: " + str(df.groupby("Prefix length")["MAE"].mean()) + "\n")
        result_f.write("Mean DL per prefix: " + str(df.groupby("Prefix length")["Damerau"].mean()) + "\n")
        result_f.write("Mean MAE: " + str(df["MAE"].mean()) + "\n")
        result_f.write("Mean DL: " + str(df["Damerau"].mean()) + "\n")


"""Arpeggio helper methods
"""

import collections
import logging
import os
import platform

import numpy as np
from openbabel import openbabel as ob
from Bio.PDB.Atom import Atom
from Bio.PDB.Residue import Residue
from arpeggio.core import SelectionError, SiftMatchError, config

# region general

def setup_filetype(filepath):
    """Sets up argument type to be consumedd by openbabel

    Args:
        filepath (str): Path to the source file

    Returns:
        str: file type
    """
    filetype = 'pdb'

    if filepath.endswith('.pdb') or filepath.endswith('.ent'):
        filetype = 'pdb'
    elif filepath.endswith('.mmcif') or filepath.endswith('.cif'):
        filetype = 'mmcif'

    return filetype

def rename_output_file(original_filename, new_extension):
    """

    Args:
        original_filename (str): [description]
        new_extension (str): [description]

    Returns:
        str: new output file name
    """
    original_extension = os.path.splitext(original_filename)[-1]

    if original_extension:
        return os.path.splitext(original_filename)[0] + new_extension

    return original_filename + new_extension

def max_mem_usage():
    """Returns maximum memory usage of the program thus far, in megabytes, as a string.

    Returns:
        str: Program message
    """

    try:
        import resource
        base = 1024.0 if platform.system() == 'Linux' else 1048576.0
        return str(round(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / base, 2)) + ' MB'
    except Exception as err:
        return 'Resource usage information not available {}'.format(str(err))
# endregion

# region bonds identification

def is_hbond(donor, acceptor, vdw_comp_factor):
    """Determines if atoms share Hbond.

    Args:
        donor (OBAtom): First atom
        acceptor (OBAtom): Second atom
        vdw_comp_factor (float): Compensation factor for VdW radii
            dependent interaction types.

    Returns:
        int: 1/0 binary information whether or not the atoms share
            Hbond.
    """
    for hydrogen_coord in donor.h_coords:
        h_dist = np.linalg.norm(hydrogen_coord - acceptor.coord)

        if h_dist <= config.VDW_RADII['H'] + acceptor.vdw_radius + vdw_comp_factor:
            if get_angle(donor.coord, hydrogen_coord, acceptor.coord) >= config.CONTACT_TYPES['hbond']['angle rad']:
                return 1

    return 0

def is_weak_hbond(donor, acceptor, vdw_comp_factor):
    """Determines if atoms share a weak Hbond.

    Args:
        donor (OBAtom): First atom
        acceptor (OBAtom): Second atom
        vdw_comp_factor (float): Compensation factor for VdW radii
            dependent interaction types.

    Returns:
        int: 1/0 binary information whether or not the atoms share
            weak Hbond.
    """
    for hydrogen_coord in donor.h_coords:
        h_dist = np.linalg.norm(hydrogen_coord - acceptor.coord)

        if h_dist <= config.VDW_RADII['H'] + acceptor.vdw_radius + vdw_comp_factor:
            if get_angle(donor.coord, hydrogen_coord, acceptor.coord) >= config.CONTACT_TYPES['weak hbond']['angle rad']:
                return 1

    return 0

def is_halogen_weak_hbond(donor, halogen, ob_mol, vdw_comp_factor, bio_to_ob, ob_to_bio):
    """Determines if atoms share a weak halogen bond.

    org. desc: `nbr` WILL BE A BIOPYTHON ATOM ... HOPEFULLY
    Args:
        donor (OBAtom): Donor atom
        halogen (OBAtom): Halogen atom
        ob_mol (OBMol): [description]
        vdw_comp_factor (float): [description]
        bio_to_ob (dict of int): biopython to openbabel mapping
        ob_to_bio (dict of int): openbabel to biopython mapping

    Returns:
        int: 1/0 binary information whether or not the atoms share
            weak weak Hbond.
    """

    # entry 5t45 obabel does not identify bond between
    # SER 246 A (OG) and BEF 903 A (F1) which makes neighbour to be
    # None and then it crashes
    neighbour = get_single_bond_neighbour(ob_mol.GetAtomById(bio_to_ob[halogen]))
    if neighbour is None:
        return 0

    nbr = ob_to_bio[neighbour.GetId()]

    for hydrogen_coord in donor.h_coords:

        h_dist = np.linalg.norm(halogen.coord - hydrogen_coord)

        if h_dist <= config.VDW_RADII['H'] + halogen.vdw_radius + vdw_comp_factor:

            if config.CONTACT_TYPES['weak hbond']['cx angle min rad'] <= get_angle(nbr.coord, halogen.coord, hydrogen_coord) <= config.CONTACT_TYPES['weak hbond']['cx angle max rad']:

                return 1

    return 0

def is_xbond(donor, acceptor, ob_mol, bio_to_ob, ob_to_bio):
    """Determines if atoms share a weak halogen bond.

    Args:
        donor (OBAtom): Donor atom
        acceptor (OBAtom): Halogen atom
        ob_mol (OBMol): [description]

    Returns:
        int: 1/0 binary information whether or not the atoms share
            xbond.
    """

    # `nbr` WILL BE A BIOPYTHON ATOM
    # ... HOPEFULLY
    nbr = ob_to_bio[get_single_bond_neighbour(ob_mol.GetAtomById(bio_to_ob[donor])).GetId()]
    theta = get_angle(nbr.coord, donor.coord, acceptor.coord)

    if theta >= config.CONTACT_TYPES['xbond']['angle theta 1 rad']:
        return 1

    return 0

def update_atom_sift(atom, addition, contact_type='INTER'):
    """[summary]

    Args:
        atom ([type]): [description]
        addition ([type]): [description]
        contact_type (str, optional): Defaults to 'INTER'. [description]
    """

    atom.sift = [x or y for x, y in zip(atom.sift, addition)]

    if contact_type == 'INTER':
        atom.sift_inter_only = [x or y for x, y in zip(atom.sift_inter_only, addition)]

    if 'INTRA' in contact_type:
        atom.sift_intra_only = [x or y for x, y in zip(atom.sift_intra_only, addition)]

    if 'WATER' in contact_type:
        atom.sift_water_only = [x or y for x, y in zip(atom.sift_water_only, addition)]

def update_atom_fsift(atom, addition, contact_type='INTER'):
    """[summary]

    Args:
        atom ([type]): [description]
        addition ([type]): [description]
        contact_type (str, optional): Defaults to 'INTER'. [description]
    """

    atom.actual_fsift = [x or y for x, y in zip(atom.actual_fsift, addition)]

    if contact_type == 'INTER':
        atom.actual_fsift_inter_only = [x or y for x, y in zip(atom.actual_fsift_inter_only, addition)]

    if 'INTRA' in contact_type:
        atom.actual_fsift_intra_only = [x or y for x, y in zip(atom.actual_fsift_intra_only, addition)]

    if 'WATER' in contact_type:
        atom.actual_fsift_water_only = [x or y for x, y in zip(atom.actual_fsift_water_only, addition)]

def update_atom_integer_sift(atom, addition, contact_type='INTER'):
    """[summary]

    Args:
        atom ([type]): [description]
        addition ([type]): [description]
        contact_type (str, optional): Defaults to 'INTER'. [description]
    """

    atom.integer_sift = [x + y for x, y in zip(atom.sift, addition)]

    if contact_type == 'INTER':
        atom.integer_sift_inter_only = [x + y for x, y in zip(atom.sift_inter_only, addition)]

    if 'INTRA' in contact_type:
        atom.integer_sift_intra_only = [x + y for x, y in zip(atom.sift_intra_only, addition)]

    if 'WATER' in contact_type:
        atom.integer_sift_water_only = [x + y for x, y in zip(atom.sift_water_only, addition)]

def sift_xnor(sift1, sift2):
    """[summary]

    Args:
        sift1 ([type]): [description]
        sift2 ([type]): [description]

    Raises:
        ValueError: [description]

    Returns:
        [type]: [description]
    """'''
    '''

    out = []

    for x, y in zip(sift1, sift2):

        if x and not y:  # TF
            out.append(0)

        elif not x and not y:  # FF
            out.append(1)

        elif x and y:  # TT
            out.append(1)

        elif not x and y:  # FT
            out.append(0)

        else:
            raise ValueError('Invalid SIFts for matching: {} and {}'.format(sift1, sift2))

    return out

def sift_match_base3(sift1, sift2):
    """[summary]
    0 = UNMATCHED
    1 = MATCHED
    2 = MATCH NOT POSSIBLE

    Assuming that sift1 is the potential SIFt, and sift2 is the actual SIFt.
    Args:
        sift1 ([type]): [description]
        sift2 ([type]): [description]

    Raises:
        SiftMatchError: [description]
        ValueError: [description]

    Returns:
        [type]: [description]
    """

    out = []

    for x, y in zip(sift1, sift2):

        if x and not y:  # TF
            out.append(0)  # UNMATCHED

        elif not x and not y:  # FF
            out.append(2)  # MATCH NOT POSSIBLE

        elif x and y:  # TT
            out.append(1)  # MATCHED

        elif not x and y:  # FT
            raise SiftMatchError

        else:
            raise ValueError('Invalid SIFts for matching: {} and {}'.format(sift1, sift2))

    return out

def human_sift_match(sift_match, feature_sift=config.FEATURE_SIFT):
    """Takes a base-3 SIFt indicating contact matched-ness and converts it to a human readable form.

    Args:
        sift_match ([type]): [description]
        feature_sift ([type], optional): Defaults to config.FEATURE_SIFT. [description]

    Raises:
        ValueError: [description]

    Returns:
        [type]: [description]
    """

    terms = []

    for e, fp in enumerate(sift_match):

        if fp == 2:  # MATCH NOT POSSIBLE
            continue

        elif fp == 1:
            terms.append('Matched {}'.format(feature_sift[e]))

        elif fp == 0:
            terms.append('Unmatched {}'.format(feature_sift[e]))

        else:
            raise ValueError

    terms.sort()
    return ':'.join(terms)
# endregion

def int2(x):
    '''
    Return integer from base 2 number.

    Can accept a list/tuple of 0s and 1s.
    '''

    if isinstance(x, collections.Iterable):
        x = ''.join([str(k) for k in x])
    else:
        x = str(x)

    return int(x, 2)

def is_digit(x):
    try:
        int(x)
        return True
    except ValueError:
        return False

def int3(x):
    '''
    Return integer from base 3 number.

    Can accept a list/tuple of 0s, 1s and 2s.
    '''

    if isinstance(x, collections.Iterable):
        x = ''.join([str(k) for k in x])
    else:
        x = str(x)

    return int(x, 3)

def selection_parser(selection_list, atom_list):
    '''
    Selection syntax:

    /<chain_id>/<res_num>[<ins_code>]/<atom_name>

    HET:<het_id>

    Other formats will be rejected, for now.
    You can omit fields as long as the number of `/` is correct.

    Selections are additive, so if you select chain A with /A//,
    adding /A/91/C23 won't make any difference.
    '''

    final_atom_list = set([])

    for selection in selection_list:

        # selection_dict = {
        #    'chain': None,
        #    'residue_number': None,
        #    'atom_name': None
        # }

        chain = None
        residue_number = None
        insertion_code = ' '
        # residue_range = None # TODO
        atom_name = None

        current_atom_list = atom_list[:]

        original_selection = selection
        selection = selection.strip()

        if selection.startswith('RESNAME:'):

            selection = selection.replace('RESNAME:', '').strip()

            # RESNAMES ARE MAX LENGTH 3
            if len(selection) > 3:
                raise SelectionError(original_selection)

            current_atom_list = [x for x in current_atom_list if x.get_parent().resname.strip() == selection]

            for selected_atom in current_atom_list:
                final_atom_list.add(selected_atom)

        # SELECT ALL ORGANIC SMALL-MOLECULE LIGANDS
        elif selection.startswith('LIGANDS'):

            current_atom_list = [x for x in current_atom_list if
                                 not x.get_parent().is_polypeptide  # MUST NOT BE POLYPEPTIDE
                                 and len(x.get_parent().child_list) >= 5  # MIN NUMBER OF ATOMS
                                 and len(x.get_parent().child_list) <= 100  # MAX NUMBER OF ATOMS
                                 and 'C' in set([y.element for y in x.get_parent().child_list])  # MUST CONTAIN CARBON
                                 and x.get_parent().resname.strip().upper() not in config.COMMON_SOLVENTS  # MUST NOT BE COMMON SOLVENT
                                 and x.get_parent().resname.strip().upper() not in config.STANDARD_NUCLEOTIDES  # MUST NOT BE NUCLEOTIDE
                                 and not x.get_parent().resname.startswith('+')  # MUST NOT BE MODIFIED NUCLEOTIDE
                                 ]

            for selected_atom in current_atom_list:
                final_atom_list.add(selected_atom)

        elif selection.startswith('/'):

            selection = selection.lstrip('/').split('/')

            # print selection

            if len(selection) != 3:
                raise SelectionError(original_selection)

            # CHAIN
            if selection[0]:
                chain = selection[0]

            # RESIDUE AND INS CODE
            if selection[1]:

                if is_digit(selection[1]):

                    # JUST THE RESNUM
                    residue_number = int(selection[1])

                elif selection[1].isalnum():

                    # CHECK FOR VALID RESNUM+INSCODE
                    if selection[1][-1].isalpha() and is_digit(selection[1][:-1]):

                        residue_number = int(selection[1][:-1])
                        insertion_code = selection[1][-1]

                    else:
                        raise SelectionError(original_selection)

                else:
                    raise SelectionError(original_selection)

            # ATOM NAME
            if selection[2]:

                if not selection[2].isalnum() and "'" not in selection[2]:
                    raise SelectionError(original_selection)
                else:
                    atom_name = selection[2]

            # NOW MAKE THE SELECTION WITH BIOPYTHON
            if chain:
                current_atom_list = [x for x in current_atom_list if x.get_parent().get_parent().id == chain]

            if residue_number:
                current_atom_list = [x for x in current_atom_list if x.get_parent().id[1] == residue_number and x.get_parent().id[2] == insertion_code]

            if atom_name:
                current_atom_list = [x for x in current_atom_list if x.name == atom_name]

            for selected_atom in current_atom_list:
                final_atom_list.add(selected_atom)

        else:
            raise SelectionError(original_selection)

    final_atom_list = list(final_atom_list)

    if not final_atom_list:
        logging.error('Selection was empty.')
        raise SelectionError('entity not found')

    return list(final_atom_list)

def make_pymol_json(entity):
    '''
    Feed me a BioPython atom or BioPython residue.

    See `http://pymol.sourceforge.net/newman/user/S0220commands.html`.

    chain-identifier/resi-identifier/name-identifier
    chain-identifier/resi-identifier/
    '''

    if isinstance(entity, Atom):
        chain = entity.get_parent().get_parent()
        residue = entity.get_parent()
        atom_name = entity.name

        return {
            'label_comp_id': residue.resname,
            'auth_seq_id': residue.id[1],
            'auth_asym_id': chain.id,
            'auth_atom_id': atom_name,
            'pdbx_PDB_ins_code': residue.id[2]
        }

    elif isinstance(entity, Residue):
        chain = entity.get_parent()

        return {
            'label_comp_id': entity.resname,
            'auth_seq_id': entity.id[1],
            'auth_asym_id': chain.id,
            'pdbx_PDB_ins_code': entity.id[2]
        }

    else:
        raise TypeError('Cannot make a json object from non-Atom/Residue object.')

def make_pymol_string(entity):
    """Feed me a BioPython atom or BioPython residue.

    See `http://pymol.sourceforge.net/newman/user/S0220commands.html`.

    chain-identifier/resi-identifier/name-identifier
    chain-identifier/resi-identifier/

    Args:
        entity ([type]): [description]

    Raises:
        TypeError: [description]

    Returns:
        str: String representation of the entity
    """

    if isinstance(entity, Atom):

        chain = entity.get_parent().get_parent()
        residue = entity.get_parent()
        atom_name = entity.name

    elif isinstance(entity, Residue):
        chain = entity.get_parent()
        residue = entity
        atom_name = ''

    else:
        raise TypeError('Cannot make a PyMOL string from a non-Atom or Residue object.')

    res_num = residue.id[1]

    # ADD INSERTION CODE IF NEED BE
    if residue.id[2] != ' ':
        res_num = str(res_num) + residue.id[2]

    macro = '{}/{}/{}'.format(chain.id,
                              res_num,
                              atom_name)

    return macro

def get_single_bond_neighbour(ob_atom):
    """[summary]

    Args:
        ob_atom ([type]): [description]

    Returns:
        [type]: [description]
    """'''
    '''

    for bond in ob.OBAtomBondIter(ob_atom):

        if not (bond.GetBondOrder()==1 and not bond.IsAromatic()):
            continue

        current_neighbour = bond.GetNbrAtom(ob_atom)

        if current_neighbour.GetAtomicNum() == 1:
            continue

        return current_neighbour

    return None

def group_angle(group, point_coords, degrees=False, signed=False):
    """
    Adapted from CREDO: `https://bitbucket.org/blundell/credovi/src/bc337b9191518e10009002e3e6cb44819149980a/credovi/structbio/aromaticring.py?at=default`

    `group` should be a dict with Numpy array 'center' and 'normal' attributes.
    `point_coords` should be a Numpy array.
    """

    cosangle = np.dot(group['normal'], point_coords) / (np.linalg.norm(group['normal']) * np.linalg.norm(point_coords))

    # GET THE ANGLE AS RADIANS
    rad = np.arccos(cosangle)

    if not degrees:
        return rad

    # CONVERT RADIANS INTO DEGREES
    # CONVERT INTO A SIGNED ANGLE
    if signed:
        rad = rad - np.pi if rad > np.pi / 2 else rad

    # RETURN DEGREES
    return rad * 180 / np.pi

def group_group_angle(group, group2, degrees=False, signed=False):
    """Adapted from CREDO: `https://bitbucket.org/blundell/credovi/src/bc337b9191518e10009002e3e6cb44819149980a/credovi/structbio/aromaticring.py?at=default`

    `group` and `group2` should be a dict with Numpy array 'center' and 'normal' attributes.
    Args:
        group ([type]): [description]
        group2 ([type]): [description]
        degrees (bool, optional): Defaults to False. [description]
        signed (bool, optional): Defaults to False. [description]

    Returns:
        [type]: [description]
    """

    cosangle = np.dot(group['normal'], group2['normal']) / (np.linalg.norm(group['normal']) * np.linalg.norm(group2['normal']))

    # GET THE ANGLE AS RADIANS
    rad = np.arccos(cosangle)

    if not degrees:
        return rad

    # CONVERT RADIANS INTO DEGREES
    else:

        # CONVERT INTO A SIGNED ANGLE
        if signed:
            rad = rad - np.pi if rad > np.pi / 2 else rad

        # RETURN DEGREES
        return rad * 180 / np.pi

def get_angle(point_a, point_b, point_c):
    """Get the angle between three points in 3D space.
    Points should be supplied in Numpy array format.

    http://stackoverflow.com/questions/19729831/angle-between-3-points-in-3d-space

    Args:
        point_a ([type]): [description]
        point_b ([type]): [description]
        point_c ([type]): [description]

    Returns:
        [type]: [description]
    """
    # In pseudo-code, the vector BA (call it v1) is:
    # v1 = {A.x - B.x, A.y - B.y, A.z - B.z}
    v1 = point_a - point_b

    # Similarly the vector BC (call it v2) is:
    # v2 = {C.x - B.x, C.y - B.y, C.z - B.z}
    v2 = point_c - point_b

    # The dot product of v1 and v2 is a function of the cosine of the angle between them
    # (it's scaled by the product of their magnitudes). So first normalize v1 and v2:

    # v1mag = sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z)
    v1_mag = np.sqrt(v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2])

    # v1norm = {v1.x / v1mag, v1.y / v1mag, v1.z / v1mag}
    v1_norm = np.array([v1[0] / v1_mag, v1[1] / v1_mag, v1[2] / v1_mag])

    # v2mag = sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z)
    v2_mag = np.sqrt(v2[0] * v2[0] + v2[1] * v2[1] + v2[2] * v2[2])

    # v2norm = {v2.x / v2mag, v2.y / v2mag, v2.z / v2mag}
    v2_norm = np.array([v2[0] / v2_mag, v2[1] / v2_mag, v2[2] / v2_mag])

    # Then calculate the dot product:

    # res = v1norm.x * v2norm.x + v1norm.y * v2norm.y + v1norm.z * v2norm.z
    res = v1_norm[0] * v2_norm[0] + v1_norm[1] * v2_norm[1] + v1_norm[2] * v2_norm[2]

    # And finally, recover the angle:
    angle = np.arccos(res)

    if np.isnan(angle):
        logging.warning(f'Angle for <{point_a}, {point_b}, {point_c}> was NaN, setting to PI.')
        angle = np.pi

    return angle

def get_residue_name(entity):
    """
    Returns the name of residue

    Args:
        entity (Atom/Residue): Atom or Residue objects from BioPython

    Returns:
        str: Name of the residue
    """

    if isinstance(entity, Atom):
        residue = entity.get_parent()
        return residue.get_resname()

    elif isinstance(entity, Residue):
        return entity.get_resname()

    else:
        raise TypeError('Cannot return Residue from from non-Atom/non-Residue object.')


# flask_simple_server
import atexit
import glob
import logging
import os
import subprocess
import socket
import sys
import threading
import time
import json
import requests
from multiprocessing import Process
from os import path
from threading import Timer

from flask import Flask, request
from requests import exceptions, get, post

import backendManager as bm
import check_if_port_open
import obs_websocket as obs
import steamVRManager as svrm
import systemSettings as sys
import udp_client_listener as ucl
# -----------------------------
import checkInstalledGame as ch

app = Flask(__name__)
logging.basicConfig(level=logging.DEBUG)
# app_id
TEAMVR_GAMEID = '250820'
HELLBLADE = '747350'
STEAMVR_ID = '250820'
# GAME_NAME
SteamVR = 'vrserver'

# IP
CLOUDXR_SERVER_MANAGER_IP = ''
CLOUDXR_CLIENT_IP = ''
BACKEND_SERVER_IP = ''
# FLAG
is_available = True
app_id = ''
app_title = ''
player_ip = ''
platform = ''
applist = list()
# GlOBAL VARIABLES
launch_time = time.time()
g_CurrentGameID = ''
settings = ''

@app.route('/connection-timeout', methods=['GET'])
def timeoutCloudXR():
    svrm.timeoutOpenGame(BACKEND_SERVER_IP)
    global is_available
    is_available = True
    print("timeoutCloudXR")

# check if steamvr is ok and return ready for a user to connect
@app.route('/game-connection', methods=['POST', 'GET'])
def launchCloudXR():
    if request.method == 'POST':
        print('============launchCloudXR=============')
        global player_ip, is_available, app_title, platform
        global app_id
        global BACKEND_SERVER_IP
        # if not is_available:
        #     return 'Game server not available now'
        app_title = request.form.get('app_title', type=str)
        app_id = request.form.get('app_id', type=str)
        player_ip = request.form.get('player_ip', type=str)
        platform = request.form.get('platform', type = str)
        print('platform: ', platform)

        svrm.setAppID(app_id)
        svrm.setAppTitle(app_title)
        svrm.setAppPlatform(platform)

        # logging.info("request: {0} {1} {2}".format(app_title,  app_id, player_ip))

        if svrm.CheckGameStatus(SteamVR) >= 1:
            if platform == "steam" or platform == "compal":
                if is_available == True:
                    svrm.startGame(BACKEND_SERVER_IP, player_ip,
                                app_id, app_title, platform)
                is_available = False
                return {'launch success': True}
            else:
                return {'launch success': False}
        else:
            svrm.startSteamvr(STEAMVR_ID)
            while svrm.CheckGameStatus(SteamVR) == 0:
                continue
            # TODO: open steamvr again and startGame
            # TODO: return false if error happen
            return {'launch success': False}
    # for launching steamVR process
    else:
        #should return exception when packet is not POST
        if time.time() > launch_time+10:
            return {'status': True}
        else:
            return {'status': False}

@app.route('/game-disconnection', methods=['POST'])
def closeCloudXR():
    global player_ip, is_available, app_id, app_title,platform
    # print(player_ip, request.remote_addr)
    # app_title = request.form.get('app_title', type=str)
    # app_id = request.form.get('app_id', type=str)
    # if request.remote_addr != player_ip and player_ip != '127.0.0.1':
    #     return 'Invalid request'

    # Close game app and steamVR, and reset game server status
    print('closeCloudXR : ', svrm.getAppTitle())
    logging.info(svrm.getAppTitle())
    # if g_CurrentGameTitle == '':
    #     return {'close fail': False, 'app_title' : app_title}
    if svrm.CheckGameStatus(svrm.getAppTitle()) >= 1:
        #close the current game
        cur_platform = svrm.getAppPlatform()
        # print('cur_platform: ', cur_platform)
        if cur_platform == 'steam':
            svrm.closeGame(svrm.getAppTitle())
        elif cur_platform =='compal':
            print('svrm.getApplication: ', svrm.getApplication())
            svrm.closeApplication(svrm.getAppTitle(), svrm.getApplication())
        #close the steamvr
        time.sleep(5)
        svrm.closeSteamvr()
        #close obs streaming
        stream_server_ip = settings['STREAM_SERVER_IP']
        obs.stop(BACKEND_SERVER_IP,stream_server_ip)
        #set game server to be available again
        is_available = True
        #print(player_ip, is_available)
        time.sleep(5)
        #restart steamvr
        svrm.startSteamvr(STEAMVR_ID)
        while svrm.CheckGameStatus(SteamVR) == 0:
            continue
        #send status to backend server
        bm.SendGameConnection(BACKEND_SERVER_IP, player_ip, app_id, "closed",platform)
        svrm.setAppID('')
        player_ip = ''

        # RegisterToBackednServer()
        return {'close success': True, 'app_title': app_title}
    elif svrm.CheckGameStatus(SteamVR) >= 1:
        svrm.closeSteamvr()
        bm.SendGameConnection(BACKEND_SERVER_IP, player_ip, app_id, "closed",platform)
        svrm.setAppID('')
        player_ip = ''
        #add delay
        time.sleep(5)
        #restart steamvr
        svrm.startSteamvr(STEAMVR_ID)
        while svrm.CheckGameStatus(SteamVR) == 0:
            continue
        return {'close success': True, 'app_title': app_title}
    else:
        return {'close success': False, 'app_title': app_title}

@app.route('/steamvr-status', methods=['POST', 'GET'])
def getSteamVRStatus():
    if svrm.CheckGameStatus(SteamVR) >= 1:
        return {'SteamVR Status': True}
    else:
        return {'SteamVR Status': False}

@app.route('/game-status', methods=['POST', 'GET'])
def getGameStatus():
    # app_id = request.form.get('app_id', type=str)
    app_title = request.form.get('app_title', type=str)
    if svrm.CheckGameStatus(app_title) >= 1:
        return {'app_id Status': True, 'app_title': app_title}
    else:
        return {'app_id Status': False, 'app_title': app_title}

@app.route('/reconnect_to_backend_server', methods=['POST', 'GET'])
def reconnect_to_backend_server():
    bm.RegisterToBackendServer(BACKEND_SERVER_IP, applist)
    return {'app_id Status': True}

@app.route('/start_droid_cam', methods=['POST', 'GET'])
def start_droid_cam():
    app_title = request.form.get('app_title', type=str)
    app_type = request.form.get('app_type', type = str)
    print('start_droid_cam app_title: ', app_title)
    print('start_droid_cam app_type: ', app_type)
    with open(os.path.relpath('GamingServer/appdict.json')) as f:
        data = json.load(f)
    for json_dict in data:
        if json_dict['App'] == app_title:
            svrm.setApplication(json_dict['Application'])
            #request the ip
            my_params = {
                    'protocol': app_type,
                    'app_name': app_title
                    }
            url = 'http://' + BACKEND_SERVER_IP + '/dataflow'
            r = requests.get(url, params=my_params)
            print("status_code: ", r.status_code)
            json_data = json.loads(r.text)
            # start camera
            if json_data['status'] == True:
                path = json_dict['DroidCam_Path']
                subprocess.Popen([ path, '-c', json_data['source_url'], '4747', '-video'])
    return json_data

@app.route('/obs_start_streaming', methods=['POST', 'GET'])
def obs_start_streaming():
    global player_ip
    print("obs_start_streaming")
    stream_server_ip = settings['STREAM_SERVER_IP']
    obs.start(BACKEND_SERVER_IP,stream_server_ip ,player_ip)
    dict = {
        'status' : True,
        'msg': 'TODO'
    }
    return dict

@app.route('/obs_stop_streaming', methods=['POST', 'GET'])
def obs_stop_streaming():
    print("obs_stop_streaming")
    stream_server_ip = settings['STREAM_SERVER_IP']
    obs.stop(BACKEND_SERVER_IP,stream_server_ip)
    dict = {
        'status' : True,
        'msg': 'TODO'
    }
    return dict

def main():
    global BACKEND_SERVER_IP
    global applist
    global settings
    # check installed steamvr games in pc
    ch.CheckInstalledSteamGame(applist)
    ch.CheckInstalledNonSteamApplication(applist)
    print(applist)
    if len(applist) == 0:
        print('applist is empty')
    # ucl.start_udp_server()
    settings = sys.loadConfig()
    # if sys not true return error
    BACKEND_SERVER_IP = settings['BACKEND_SERVER_IP']
    # register to backed server
    bm.RegisterToBackendServer(BACKEND_SERVER_IP, applist)
    # if fail to RegisterToBackednServer

    # then throw exceptions

    # if steamVR not open
    if svrm.CheckGameStatus(SteamVR) == 0:
        print("SteamVR is not running, open now!")
        svrm.startSteamvr(STEAMVR_ID)
        while svrm.CheckGameStatus(SteamVR) == 0:
            continue
    else:
        print("SteamVR is running!")

    atexit.register(bm.UnregisterToBackendServer, BACKEND_SERVER_IP)
    # atexit.register(ucl.stop_udp_server)
    # create a while-while to listen to steamvr messages
    # CheckSteamVRLogs
    # if logs updated
    # send status to backed server
    # loop

if __name__ == '__main__':
    # set host='0.0.0.0', port=80 to enable external access in local network
    # app.debug = True
    # app.use_reloader=False
    main()
    # check_if_port_open.check(
    #     settings['GAMESERVER_IP'], settings['GAMESERVER_PORT'])

    # send Heartbeat to backend
    if False:
        dict = {'BACKEND_SERVER_IP': settings['GAMESERVER_IP'], 'client_ip': 'client_ip',
                'app_id': 'app_id', 'app_title': 'app_title'}
        p = Process(target=bm.SendSystemHeartbeat, args=(dict,))
        p.start()

    # add thread to send heartbeat to backend
    app.run(host=settings['GAMESERVER_IP'], port=settings['GAMESERVER_PORT'])


# from sklearn.feature_selection import VarianceThreshold
import pandas as pd
# import pickle
# import joblib
# from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
# from sklearn.linear_model import LinearRegression, Lasso
from sklearn.linear_model import RidgeClassifier
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import mean_squared_error
from sklearn.neighbors import KNeighborsClassifier
import contextlib
import time
# import numba
# from sklearn.tree import DecisionTreeClassifier
# from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import AdaBoostClassifier, \
    GradientBoostingClassifier, \
    RandomForestClassifier
from sklearn.neural_network import MLPClassifier
# from sklearn.feature_selection import SelectKBest
# from sklearn.feature_selection import chi2
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns
# from xgboost import XGBRegressor
from xgboost import XGBClassifier
# from sklearn.preprocessing import LabelEncoder
# from sklearn.datasets import make_regression
# from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.model_selection import GridSearchCV
# from utils import visualise_predictions
# from sklearn.metrics import accuracy_score
# from sklearn.metrics import precision_score
# from sklearn.metrics import recall_score
# from sklearn.metrics import make_scorer
from sklearn.linear_model import SGDClassifier
# from sklearn.ensemble import ExtraTreesClassifier
# import statsmodels.api as sm
# from sklearn.feature_selection import SelectFromModel
# from sklearn.svm import LinearSVC

'''
class LinearRegression:
    def __init__(self, n_features: int):  # initialize parameters
        np.random.seedDTC = DecisionTreeClassifier(random_state = 11,
        max_features = "auto", class_weight = "auto",max_depth = None)(10)
        self.W = np.random.randn(n_features, 1)  # randomly initialise weight
        self.b = np.random.randn(1)  # randomly initialise bias

    def __call__(self, X):
# how do we calculate output from an input in our model?
        ypred = np.dot(X, self.W) + self.b
        return ypred  # return prediction

    def update_params(self, W, b):
        self.W = W
        # set this instance's weights
        # to the new weight value passed to the function
        self.b = b  from sklearn.metrics import mean_squared_error
do the same for the bias

df = fetch_california_housing(as_frame=True)['data']

X, y = fetch_california_housing(return_X_y=True)GradientBoostingRegressor
print(X_train.shape, y_train.shape)

model = LinearRegression(n_features=8)  # instantiate our linear model
y_pred = model(X_test)  # make prediction on data
print("Predictions:\n", y_pred[:10])  # print first 10 predictions
'''

@contextlib.contextmanager
def timer(function):
    start = time.time()
    yield
    print(f"Elapsed time for {function.__name__}: {(time.time() - start)}")

def plot_predictions(y_pred, y_true, title):
    samples = len(y_pred)
    plt.figure()
    plt.title(title)
    plt.scatter(np.arange(samples), y_pred, c='r', label='predictions')
    plt.scatter(np.arange(samples), y_true, c='b',
                label='true labels', marker='x')
    plt.legend()
    plt.xlabel('Sample numbers')
    plt.ylabel('Values')
    plt.show()

# def mean_squared_error(y_pred, y_true):
# # define our criterion (loss function)
#     errors = y_pred - y_true  # calculate errors
#     squared_errors = errors ** 2  # square errors
#     return np.mean(squared_errors)

# def minimize_loss(X_train, y_train):
#     X_with_bias = np.hstack((np.ones((X_train.shape[0], 1)), X_train))
#     optimal_w = np.matmul(
#         np.linalg.inv(np.matmul(X_with_bias.T, X_with_bias)),
#         np.matmul(X_with_bias.T, y_train),
#     )from sklearn.metrics import classification_report, confusion_matrix
#     return optimal_w[1:], optimal_w[0]

# weights, bias = minimize_loss(X_train, y_train)
# print(weights, bias)
# cost = mean_squared_error(y_pred, y_train)
# print(cost)

# model.update_params(weights, bias)
# y_pred = model(X_train)
# cost = mean_squared_error(y_pred, y_train)
# print(cost)

# sel = VarianceThreshold(threshold=(.8 * (1 - .8)))
# X = sel.fit_transform(X)

# X = SelectKBest(chi2, k=20).fit_transform(X, y)

# X_validation, X_test, y_validation, y_test = train_test_split(
#     X_test, y_test, test_size=0.5
# )

models = [  # LinearRegression(),
    KNeighborsClassifier(n_neighbors=151),
    # MLPClassifier(hidden_layer_sizes=(150, 100, 50), max_iter=1000,
    #               activation='tanh', solver='adam', random_state=1,
    #               learning_rate='adaptive'),
    # # MLPRegressor(activation='tanh', alpha=0.1,
    #    hidden_layer_sizes=(150, 100, 50),
    #    learning_rate='adaptive', solver='sgd',
    #    max_iter=1000),
    # DecisionTreeClassifier(random_state=1,
    #  max_features="sqrt",
    #  max_depth=None),
    # DecisionTreeRegressor(criterion='squared_error',
    # max_depth=5),
    # Lasso(alpha=0.00023),
    AdaBoostClassifier(learning_rate=1.0, n_estimators=10000),
    # AdaBoostRegressor(learning_rate=0.01, n_estimators=10000),
    RandomForestClassifier(
        criterion='entropy', max_depth=128,
        max_features='log2', n_estimators=1024),
    # RandomForestRegressor(criterion='poisson',
    # max_depth=12, max_features='log2',
    # n_estimators=256),
    GradientBoostingClassifier(criterion='friedman_mse',
                               learning_rate=0.2, loss='log_loss',
                               max_depth=8, max_features='sqrt',
                               min_samples_leaf=0.1,
                               min_samples_split=0.18,
                               n_estimators=10, subsample=1),
    # GradientBoostingRegressor(criterion='friedman_mse',
    # learning_rate=0.2, loss='squared_error',
    # max_depth=8, max_features='log2',
    # min_samples_leaf=0.1,
    # min_samples_split=0.18,
    # n_estimators=10, subsample=1),
    XGBClassifier(learning_rate=0.01, max_depth=6, n_estimators=324),
    # XGBRegressor(learning_rate=0.05, max_depth=4, n_estimators=220),
    SGDClassifier(alpha=0.01, loss='log_loss', penalty='none'),
    # SGDRegressor(alpha=0.01, loss='squared_error', penalty='none'),
    RidgeClassifier()
]

def model_comparisons(models, columns=None):
    data = pd.read_csv('cleaned_dataset.csv')
    y = data['Result'].values
    X = data.drop(['Result', 'Date_New', 'Link'], inplace=False, axis=1)
    X.League = X.League.astype('category').cat.codes
    if columns is None:
        columns = X.columns
    X_sc = scale_array(X[columns])
    X_train, X_test, y_train, y_test = feature_selection(columns)
    for model in models:
        model = model.fit(X_train, y_train)
        y_pred = model.predict(X_test)
        plot_predictions(y_pred[:10], y_test[:10], model)
        print(f'Mean Squared Error:{mean_squared_error(y_test, y_pred)}')
        score = model.score(X_sc, y)
        print(f'Score: {score}')
        try:
            cm = confusion_matrix(y_test, y_pred)
            print(cm)
            print(
                f'Classification Report:  \
                    {classification_report(y_test, y_pred)}')
            plt.figure(figsize=(6, 6))
            sns.heatmap(cm, annot=True, fmt="d", linewidths=.5,
                        square=True, cmap="Blues_r")
            plt.ylabel("Actual label")
            plt.xlabel("Predicted label")
            all_sample_title = f"Accuracy Score:{score}"
            plt.title(all_sample_title, size=15)
            print(accuracy(cm))
        except ValueError:
            pass
        # print(accuracy_score(y_pred, y_test))
        print(sum(y_pred.round() == y_test)/len(y_pred))
        # dump(model, f'{str(model)}.joblib')

def MLPGridSearch(X_sc, y):
    mlp_gs = MLPClassifier(max_iter=1000)
    parameter_space = {
        'hidden_layer_sizes': [(10, 30, 10), (20,), (150, 100, 50)],
        'activation': ['tanh', 'relu'],
        'solver': ['sgd', 'adam'],
        'alpha': [0.0001, 0.05],
        'learning_rate': ['constant', 'adaptive'],
    }
    clf = GridSearchCV(mlp_gs, parameter_space, n_jobs=-1, cv=5)
    clf.fit(X_sc, y)  # X is train samples and y is the corresponding labels

def scale_array(df):
    scaler = MinMaxScaler()
    scaler.fit(df)
    X_sc = scaler.transform(df)
    return X_sc

def grid_search(estimator, parameters, columns):
    '''
    param_test1 = {
    "loss":["squared_error","absolute_error"],
    "learning_rate": [0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2],
    "min_samples_split": np.linspace(0.1, 0.5, 6),
    "min_samples_leaf": np.linspace(0.1, 0.5, 6),
    "max_depth":[3,5,8],
    "max_features":["log2","sqrt"],
    "criterion": ["friedman_mse",  "squared_error"],
    "subsample":[0.5, 0.618, 0.8, 0.85, 0.9, 0.95, 1.0],
    "n_estimators":[10]
    }
    '''
    X_train, X_test, y_train, y_test = feature_selection(columns)
    gsearch1 = GridSearchCV(estimator=estimator,
                            param_grid=parameters, n_jobs=4, cv=5)
    gsearch1.fit(X_train, y_train)
    gsearch1.best_params_

important_columns = ['Season', 'Home_Team_Goals_For_This_Far',
                     'Home_Team_Goals_Against_This_Far',
                     'Away_Team_Goals_For_This_Far',
                     'Away_Team_Goals_Against_This_Far',
                     'Home_Team_Points',
                     'Away_Team_Points', 'Elo_home', 'Elo_away', 'Capacity',
                     'Home_Team_Yellows_This_Far',
                     'Away_Team_Yellows_This_Far']

important_features = [0, 3, 4, 5, 6, 7, 8, 15, 16, 17, 20, 22]

def feature_selection(features):
    data = pd.read_csv('cleaned_dataset.csv')
    y = data['Result'].values
    X = data.drop(['Result', 'Date_New', 'Link'], inplace=False, axis=1)
    X.League = X.League.astype('category').cat.codes
    X = X[features].values
    X_sc = scale_array(X)
    X_train, X_test, y_train, y_test = train_test_split(X_sc, y, test_size=0.1)
    return X_train, X_test, y_train, y_test

def accuracy(confusion_matrix):
    diagonal_sum = confusion_matrix.trace()
    sum_of_all_elements = confusion_matrix.sum()
    return diagonal_sum / sum_of_all_elements

def feature_select_RF():
    model = RandomForestClassifier()
    data = pd.read_csv('cleaned_dataset.csv')
    y = data['Result'].values
    X = data.drop(['Result', 'Date_New', 'Link'], inplace=False, axis=1)
    X_train, X_test, y_train, y_test = feature_selection(important_columns)
    model = model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    plot_predictions(y_pred[:10], y_test[:10], model)
    print(f'Mean Squared Error:{mean_squared_error(y_test, y_pred)}')
    score = model.score(scale_array(X[important_columns]), y)
    print(f'Score: {score}')
    try:
        cm = confusion_matrix(y_test, y_pred)
        print(cm)
        print(
            f'Classification Report:  \
                {classification_report(y_test, y_pred)}')
        plt.figure(figsize=(6, 6))
        sns.heatmap(cm, annot=True, fmt="d", linewidths=.5,
                    square=True, cmap="Blues_r")
        plt.ylabel("Actual label")
        plt.xlabel("Predicted label")
        all_sample_title = f"Accuracy Score:{score}"
        plt.title(all_sample_title, size=15)
    except ValueError:
        pass
    # print(accuracy_score(y_pred, y_test))
    print(sum(y_pred.round() == y_test)/len(y_test))

key_columns = ['Result', 'Home_Team_Goals_For_This_Far',
               'Home_Team_Goals_Against_This_Far',
               'Away_Team_Goals_For_This_Far',
               'Away_Team_Goals_Against_This_Far', 'Home_Team_Points',
               'Away_Team_Points', 'Home_Team_Losing_Streak',
               'Away_Team_Losing_Streak', 'Home_Team_Winning_Streak',
               'Away_Team_Winning_Streak', 'Home_Team_Unbeaten_Streak',
               'Away_Team_Unbeaten_Streak', 'Elo_home', 'Elo_away',
               'Home_Wins_This_Far', 'Home_Losses_This_Far',
               'Away_Wins_This_Far',
               'Away_Losses_This_Far', 'Home_Wins_This_Far_at_Home',
               'Home_Losses_This_Far_at_Home', 'Home_Wins_This_Far_Away',
               'Home_Losses_This_Far_Away', 'Away_Wins_This_Far_at_Home',
               'Away_Losses_This_Far_at_Home', 'Away_Wins_This_Far_Away',
               'Away_Losses_This_Far_Away', 'Capacity', 'Home_Yellow',
               'Away_Red',
               'Home_Points_Per_Game', 'Home_Goals_Per_Game',
               'Home_Goals_Against_Per_Game', 'Away_Points_Per_Game',
               'Away_Goals_Per_Game', 'Away_Goals_Against_Per_Game']

svm_cols = ['Season', 'Teams_in_League', 'Home_Team_Goals_For_This_Far',
            'Home_Team_Goals_Against_This_Far', 'Away_Team_Goals_For_This_Far',
            'Away_Team_Goals_Against_This_Far', 'Home_Team_Points',
            'Away_Team_Points', 'Away_Team_Winning_Streak',
            'Home_Team_Unbeaten_Streak', 'Away_Team_Unbeaten_Streak',
            'Elo_home',
            'Elo_away', 'Home_Wins_This_Far', 'Home_Draws_This_Far',
            'Home_Losses_This_Far', 'Away_Draws_This_Far',
            'Home_Wins_This_Far_at_Home', 'Home_Draws_This_Far_at_Home',
            'Home_Losses_This_Far_at_Home', 'Home_Draws_This_Far_Away',
            'Away_Wins_This_Far_at_Home', 'Away_Draws_This_Far_at_Home',
            'Away_Losses_This_Far_at_Home', 'Away_Wins_This_Far_Away',
            'Away_Draws_This_Far_Away', 'Capacity', 'Home_Yellow',
            'Away_Team_Yellows_This_Far', 'Away_Red', 'Home_Points_Per_Game',
            'Home_Goals_Per_Game', 'Home_Goals_Against_Per_Game',
            'Away_Points_Per_Game', 'Away_Goals_Per_Game',
            'Away_Goals_Against_Per_Game', 'Away_Cards_Per_Game',
            'Pitch_Match',
            'League']

def final_test():
    X = pd.read_csv('cleaned_dataset.csv')
    X = X[X.Season != 2022]
    X = X[X.League != 'eerste_divisie']
    X = X[X.Season > 2012]
    X.League = X.League.astype('category').cat.codes
    y = X['Result'].values
    X = X[svm_cols]
    X_sc = scale_array(X)
    model = RandomForestClassifier(
        criterion='entropy', max_depth=128,
        max_features='log2', n_estimators=1024)
    model = model.fit(X_sc, y)
    test_data = pd.read_csv('cleaned_results.csv')
    y_test = test_data.Result.values
    test_data = test_data[svm_cols]
    test_data.League = test_data.League.astype('category').cat.codes
    X_new = scale_array(test_data)
    y_pred = model.predict(X_new)
    cm = confusion_matrix(y_pred, y_test)
    print(cm)
    print(accuracy(cm))

# X_train, X_test, y_train, y_test = feature_selection(important_columns)
if __name__ == '__main__':
    # model_comparisons(models)
    pass