"""Script to stitch a pair of images."""
import os
import numpy as np
import cv2
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Polygon

from keypoint_matching import KeypointMatcher
from RANSAC import ImageAlignment, project_2d_to_6d, project_1d_to_2d
from utils import show_two_images

class ImageStitching:
    """Class to perform stitching of a pair of images."""

    def __init__(self) -> None:
        np.random.seed(1)
        pass

    def _get_alignment_params(self, img1, img2, show_matches=True, kpmatch_args=dict(), align_args=dict()):
        """Detects keypoints, performs matching and returns matches."""
        kp_matcher = KeypointMatcher(**kpmatch_args)
        matches, kp1, des1, kp2, des2 = kp_matcher.match(img1, img2, show_matches=show_matches)

        image_alignment = ImageAlignment(**align_args)
        best_params = image_alignment.align(
            img1, kp1, img2, kp2, matches,
            show_warped_image=True, num_matches=4, max_iter=2000, method="cv2",
        )

        return matches, kp1, des1, kp2, des2, best_params

    def _get_transform_matrix_and_inv_matrix(self, params: np.ndarray):
        assert params.shape == (6,)
        AM = np.zeros((3, 3))
        AM[0, :2] = params[:2]
        AM[1, :2] = params[2:4]
        AM[0, 2] = params[4]
        AM[1, 2] = params[5]
        AM[-1, -1] = 1.0

        M_1_to_2 = AM[:2]
        M_2_to_1 = np.linalg.inv(AM)[:2]

        return M_1_to_2, M_2_to_1

    @staticmethod
    def show_canvas(canvas, subcanvas, C2_as_viewed_from_1, X_left, Y_top, W_new, H_new):
        """Displays canvas with both images and transformed image 2 in image 1 space."""
        fig, ax = plt.subplots(1, 1, figsize=(13, 7), dpi=100)

        ax.imshow(canvas.astype(int), alpha=0.6)
        ax.imshow(subcanvas.astype(int), alpha=0.4)
        ax.scatter(C2_as_viewed_from_1[:, 0], C2_as_viewed_from_1[:, 1], c="red", label="Corners of Image 2 transformed")

        stiched_image_rect = Rectangle(
            (X_left, Y_top), W_new, H_new, fc='none', ec='limegreen', lw=1, label="Border of stiched image",
        )
        ax.add_patch(stiched_image_rect)

        title = "Image 1 (Left), Image 2 (Right) and Image 2 transformed (middle) based on estimated affine parameters."
        ax.set_title(title, fontsize=13)

        plt.legend(loc="lower right")
        plt.show()

    def stitch(self, img1, img2):
        """Main stitching function. NOTE: this expects RGB images."""
        img1_gray = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY)
        img2_gray = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY)

        # get matches and alignment parameters
        matches, kp1, des1, kp2, des2, best_params = self._get_alignment_params(img1_gray, img2_gray)

        # get transformation params as matrices
        M_1_to_2, M_2_to_1 = self._get_transform_matrix_and_inv_matrix(best_params)

        # transform image 2 in space of image 1
        rows, cols = img2_gray.shape
        img2_as_viewed_from_1 = cv2.warpAffine(img2_gray, M_2_to_1, (cols, rows))
        show_two_images(
            img2_gray, img2_as_viewed_from_1,
            title="Image 2 (Left) and Image 2 (Right) transformed into co-ordinates of image 1.",
        )

        # get corners for each image
        H1, W1 = img1_gray.shape
        C1 = np.array([[0, 0], [W1, 0], [0, H1], [W1, H1]])

        H2, W2 = img2_gray.shape
        C2 = np.array([[0, 0], [W2, 0], [0, H2], [W2, H2]])

        # get corners of image 2 in space of image 1
        best_params_2_to_1 = np.array(
            [
                M_2_to_1[0, 0], M_2_to_1[0, 1], M_2_to_1[1, 0],
                M_2_to_1[1, 1], M_2_to_1[0, 2], M_2_to_1[1, 2]]
        )
        C2_as_viewed_from_1 = np.dot(project_2d_to_6d(C2), best_params_2_to_1)
        C2_as_viewed_from_1 = project_1d_to_2d(C2_as_viewed_from_1)

        # get co-ordinates of image 2 transformed in image 1 space
        Y_top = np.max([0, C2_as_viewed_from_1[0, 1], C2_as_viewed_from_1[1, 1]])
        Y_bot = np.min([H1, C2_as_viewed_from_1[2, 1], C2_as_viewed_from_1[3, 1]])
        X_left = np.min([0, C2_as_viewed_from_1[0, 0], C2_as_viewed_from_1[3, 0]])
        X_right = np.max([W1, C2_as_viewed_from_1[1, 0], C2_as_viewed_from_1[2, 0]])
        H_new = Y_bot - Y_top
        W_new = X_right - X_left

        # show canvas containing both images and transformed corners
        H, W = 400, 800
        canvas = np.zeros((H, W, 3))
        canvas[:H1, :W1, :] = img1
        canvas[:H2, W - W2:, :] = img2

        # create another subcanvas containing warped version of image 2
        subcanvas = np.zeros((H, W, 3))
        subcanvas[:H2, :W2, :] = img2
        subcanvas = cv2.warpAffine(subcanvas, M_2_to_1, (W, H))

        self.show_canvas(canvas, subcanvas, C2_as_viewed_from_1, X_left, Y_top, W_new, H_new)

        # merge images with weighted sub over pixels
        merged_image = np.zeros(canvas.shape)
        merging_weights_1 = np.zeros(canvas.shape)
        merging_weights_2 = np.zeros(canvas.shape)
        checker = np.zeros(canvas.shape)

        C1_reordered = np.array([C1[0], C1[1], C1[3], C1[2]])
        P1 = Polygon(C1_reordered)

        C2_as_viewed_from_1_reordered = np.array(
            [C2_as_viewed_from_1[0], C2_as_viewed_from_1[1], C2_as_viewed_from_1[3], C2_as_viewed_from_1[2]]
        )
        P2 = Polygon(C2_as_viewed_from_1_reordered)

        for x in np.arange(int(X_left), int(X_right), 1):
            for y in np.arange(int(Y_top), int(Y_bot), 1):

                in_img_1 = P1.contains_point((x, y))
                in_img_2 = P2.contains_point((x, y))

                if in_img_1 and not in_img_2:
                    merging_weights_1[y, x] = 1.0
                    merging_weights_2[y, x] = 0.0

                if in_img_2 and not in_img_1:
                    merging_weights_1[y, x] = 0.0
                    merging_weights_2[y, x] = 1.0

                if in_img_1 and in_img_2:
                    merging_weights_1[y, x] = 0.5
                    merging_weights_2[y, x] = 0.5
                    checker[y, x] = 1.0

        # visualize merging weights
        fig, ax = plt.subplots(1, 2, figsize=(12, 5), constrained_layout=True)
        ax[0].axis("off")
        ax[0].imshow(merging_weights_1, cmap="gray")
        ax[0].set_title("Merging weights for image 1")
        ax[1].axis("off")
        ax[1].imshow(merging_weights_2, cmap="gray")
        ax[1].set_title("Merging weights for image 2")
        plt.show()

        # create the final stitched image
        stitched = np.multiply(canvas, merging_weights_1) + np.multiply(subcanvas, merging_weights_2)
        stitched = stitched[int(Y_top):int(Y_bot), int(X_left):int(X_right)]

        # show the stiched image
        f, ax = plt.subplots(1, 3, gridspec_kw={'width_ratios': [2, 2, 4]}, figsize=(20, 6))

        ax[0].imshow(img1)
        ax[0].set_title("Image 1", fontsize=18)
        ax[0].axis("off")

        ax[1].imshow(img2)
        ax[1].set_title("Image 2", fontsize=18)
        ax[1].axis("off")

        ax[2].imshow(stitched.astype(int))
        ax[2].set_title("Stitched image", fontsize=18)
        ax[2].axis("off")

        f.tight_layout()
        os.makedirs("./results/", exist_ok=True)
        f.savefig('./results/all_stiched.png', bbox_inches="tight")
        plt.show()

        return stitched

def demo(img1_path, img2_path):
    # load both images
    img1 = cv2.imread(img1_path)
    img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)

    img2 = cv2.imread(img2_path)
    img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)

    # show input images
    show_two_images(img1, img2, title="Given pair of images.")

    # initialize stitcher
    image_stitching = ImageStitching()
    stitched = image_stitching.stitch(img1, img2)

    return stitched

if __name__ == "__main__":
    stitched = demo("left.jpg", "right.jpg")

lab4-Image Alignment and Stitching/stitch.py


'''um programa que tenha uma função chamada voto() que vai receber como parâmetro o ano de nascimento de uma pessoa,
retornando um valor literal indicando se uma pessoa tem voto NEGADO, OPCIONAL e OBRIGATÓRIO nas eleições'''

from datetime import date
def status():
    global AnoNaci
    anoAtual = date.today().year
    idade = anoAtual - AnoNaci
    if idade < 16:
        print(f'\033[0;31mCom {idade} anos de idade, você não pode votar ainda.')
    if 15 < idade < 18 or idade > 69:
        print(f'\033[0;32mCom {idade} anos de idade, seu voto é opcional.')
    if 17 < idade < 70:
        print(f'\033[0;33mCom {idade} anos de idade, seu voto é obrigatorio.')

# Programa principal
print('Saiba aqui seu status eleitoral!')
print('+=' * 20)

AnoNaci = int(input('Qual o ano do seu nacimento? '))
status()


import base64
import os
import pathlib
import time
from typing import AnyStr

import click

from common import api_utility
from common import chronicle_auth
from common import exception_handler
from common import options
from common.constants import key_constants as common_constants
from common.constants import status
from parsers import url
from parsers.constants import key_constants
from parsers.constants import path_constants

@click.command(
    name="download", help="Download parser code given config ID or log type")
@options.env_option
@options.region_option
@options.verbose_option
@options.credential_file_option
@exception_handler.catch_exception()
def download(credential_file: AnyStr, verbose: bool, region: str,
             env: str) -> None:
  """Download parser code given log type or config ID.

  Args:
    credential_file (AnyStr): Path of Service Account JSON.
    verbose (bool): Option for printing verbose output to console.
    region (str): Option for selecting regions. Available options - US, EUROPE,
      ASIA_SOUTHEAST1.
    env (str): Option for selecting environment. Available options - prod, test.

  Raises:
    OSError: Failed to read the given file, e.g. not found, no read access
      (https://docs.python.org/library/exceptions.html#os-exceptions).
    ValueError: Invalid file contents.
    KeyError: Required key is not present in dictionary.
    TypeError: If response data is not JSON.
  """
  click.echo(
      "Note: If you want to download parser by log type then skip the config ID."
  )
  config_id = click.prompt("Enter config ID", show_default=False, default="")

  http_client = chronicle_auth.initialize_http_session(credential_file)
  method = "GET"

  if config_id:
    # Get the parser from config id
    download_parser_url = f'{url.get_url(region, "list", env)}/{config_id}'
    response = http_client.request(
        method, download_parser_url, timeout=url.HTTP_REQUEST_TIMEOUT_IN_SECS)
    download_parser_response = api_utility.check_content_type(response.text)
  else:
    # Get the parser of `log_type` from list of parsers
    log_type = click.prompt("Enter Log Type", show_default=False, default="")
    if not log_type:
      click.echo("Please enter log type or config ID.")
      return
    download_parser_url = url.get_url(region, "list", env)
    response = http_client.request(
        method, download_parser_url, timeout=url.HTTP_REQUEST_TIMEOUT_IN_SECS)
    download_parser_response = api_utility.check_content_type(response.text)
    if key_constants.KEY_CBN_PARSER not in download_parser_response:
      click.echo("No CBN parsers currently configured.")
      return
    found = False
    for p in download_parser_response[key_constants.KEY_CBN_PARSER]:
      if p[common_constants.KEY_LOG_TYPE] == log_type:
        found = True
        download_parser_response = p
        break
    if not found:
      click.echo(f"Parser for log type {log_type} not found.")
      return

  click.echo("Downloading parser...")

  if response.status_code != status.STATUS_OK:
    click.echo(
        f"Error while downloading parser:\nResponse Code: {response.status_code}"
        f"\nError: {download_parser_response[common_constants.KEY_ERROR][common_constants.KEY_MESSAGE]}"
    )
    return

  decoded_config = base64.b64decode(
      download_parser_response[key_constants.KEY_CONFIG])
  decoded_config = decoded_config.decode("utf-8")
  timestr = time.strftime("%Y%m%d%H%M%S")
  filename = download_parser_response[
      common_constants.KEY_LOG_TYPE] + "_" + timestr + ".conf"
  sample_dir = pathlib.Path(path_constants.PARSER_DATA_DIR)
  sample_dir.mkdir(parents=True, exist_ok=True)
  filepath = os.path.join(sample_dir, filename)
  click.echo(f"Writing parser to: {filepath}")
  with open(filepath, "w") as f:
    f.write(decoded_config)

  if verbose:
    api_utility.print_request_details(download_parser_url, method, None,
                                      download_parser_response)


from dataclasses import dataclass
import string
from typing import List, Tuple

DIGITS = tuple(string.digits)
LIMIT = 100_000
TO_DELETE = 30_000_000

@dataclass
class Directory:
    name: str
    dirs: List['Directory']
    size: int
    parent: 'Directory'

def solve(lines: List[str]) -> Tuple[int]:
    root = Directory("/", [], 0, None)
    result = []
    cwd = root
    for line in lines[1:]:
        if line.startswith("$ cd"):
            name = line.split()[2]
            if name == "..":
                if cwd.size <= LIMIT:
                    result.append(cwd)
                cwd.parent.size += cwd.size
                cwd = cwd.parent
            else:
                dir = Directory(name, [], 0, cwd)
                cwd.dirs.append(dir)
                cwd = dir
        elif line.startswith(DIGITS):
            size = int(line.split()[0])
            cwd.size += size
    while cwd.parent:
        if cwd.size <= LIMIT:
            result.append(cwd)
        cwd.parent.size += cwd.size
        cwd = cwd.parent
    return sum(d.size for d in result)

with open("input_test") as f:
    lines = f.readlines()

print(solve(lines))


import os
import openai
import pandas as pd

from secret import API_KEY
from sqlalchemy import create_engine, text
from utils import create_prompt, call_openai

os.environ["OPENAI_API_KEY"] = API_KEY
openai.api_key = os.environ["OPENAI_API_KEY"]

EXIT_KEY = "q"
ERROR_MESSAGE = "Sorry, no results ЪциЪЈ╝РђЇРЎѓ№ИЈ"

df = pd.read_json("instruments.json")
# TEMP DATABASE IN RAM
temp_db = create_engine("sqlite:///:memory:")
# PUSH PANDAS DF ---> TEMP DB
df.to_sql(name="instruments", con=temp_db)

user_input = ""
while user_input != EXIT_KEY:
    print("Enter a prompt. Enter q to quit")
    user_input = input()
    if user_input != EXIT_KEY:
        query_result = create_prompt(df, user_input) # DF + query that does ...
        response = call_openai(query_result)
        print(f"Query: {response}")

        with temp_db.connect() as conn:
            result = conn.execute(text(response))

        print("Results: *")
        print("**********")
        returned_data = result.all()
        for i, instrument in enumerate(returned_data):
            print(f"{i+1}. {instrument.name.lower().capitalize()}")
        print("**********")
print("Bye!")

# HOW TO PICK A LANGUAGE MODEL
# Think about cost of the model and how often you'll use it
# How many tokens you'll pass into the model or get back
# Importance of accuracy in results

# OpenAI Completion Call Params
# model (i.e. text-davinci-003, etc.)
# prompt
# temperature (0.0 to 1.0, the higher, the more likely the model will take risks and be creative)
# max_tokens (most tokens to use in response)
# top_p (similar to temperature)
# n (how many completions to generate)
# frequency_penalty (-2.0 to 2.0, likelihood of model repeating itself, higher means less likely, 0.1 to 1.0 is usually good enough)
# presence_penalty (-2.0 to 2.0, similar to frequency_penalty)


"""
    Misc. utility functions for manipulating files.
"""
import gzip
import hashlib
import math
import os
import re
import shutil
import string
import tarfile
import zipfile
import tempfile
from typing import Generator, BinaryIO, List, Dict, Optional

from dataclasses import dataclass
from onscale_client.api.datamodel import BlobType

# Regexp to match "chunked" files
CHUNK_PATTERN = r"\._[0-9]{5}_[0-9]{5}$"

# 256 byte stream buffer
BUFFER = 65536

# Valid characters for a file path
VALID_PATH_CHARS = set(f"-_.(){string.ascii_letters}{string.digits}")

TEMP_DIR = os.path.join(tempfile.gettempdir(), "onscale_client")

@dataclass(frozen=True)
class UploadContext:
    """Store information needed to create an upload form."""

    file_path: str
    method: str
    uri: str
    headers: Optional[Dict] = None
    fields: Optional[Dict] = None

@dataclass(frozen=True)
class FileContext:
    """Store information on a local file."""

    name: str
    dirname: Optional[str] = None
    original_name: Optional[str] = None
    uri: Optional[str] = None
    aes_key: Optional[bytes] = None
    file_hash: Optional[str] = None
    is_rtg: bool = False
    sequence_id: str = "0"

    @property
    def path(self) -> str:
        """Return full file path of this file."""
        if self.dirname is None:
            return self.name
        return os.path.join(self.dirname, self.name)

def clean_path(path: str) -> str:
    """Remove invalid characters from a file path. Replace spaces with _.

    Args:
        path: Some file name or path.

    Returns:
        Input string with characters not in `VALID_PATH_CHARS` removed.
    """
    new_path = path.replace(" ", "_")
    return "".join([c for c in new_path if c in VALID_PATH_CHARS])

def maybe_makedirs(path: str):
    """Make directory for a given path if doesn't exist

    Args:
        path: Directory name to create.
    """
    dirname = os.path.realpath(path)

    if not os.path.exists(dirname):
        try:
            os.makedirs(dirname)  # Extra safe for parallel tasks
        except FileExistsError:
            pass

def listdir(dirname: str) -> Generator[str, None, None]:
    """Similar to os.listdir, but give full relative path.

    Args:
        dirname: Directory to list contents.

    Yields:
        Paths to files in directory.
    """
    for obj in os.listdir(dirname):
        yield os.path.join(dirname, obj)

def full_listdir(dirname: str = os.getcwd(), relative: bool = True) -> List[str]:
    """Recursively list all files in all subdirectories of a directory.

    Args:
        dirname: Directory to search contents.
        relative: Make paths returned by this function relative to 'dirname'.

    Returns:
        All files in all subdirectories of `dirname`, relative to `dirname`.
    """
    # Get all files
    files = [os.path.join(d, f) for d, _, fs in os.walk(dirname) for f in fs]

    if not relative:
        return files

    files = [ltrim(f, dirname).lstrip(os.path.sep) for f in files]
    return files

def ltrim(text: str, sub: str) -> str:
    """Remove `sub` from `text` if `text` startswith `sub`.

    Args:
        text: Input text to search.
        sub: Search query.

    Returns:
        Input with query removed if present at beginning.
    """
    if text and text.startswith(sub):
        text = text[len(sub) :]
    return text

def flatten_dir(dirname: str):
    """Flatten all files from subdirectories into parent directory.

    Args:
        dirname: Top-level directory to flatten.
    """

    def recurse(_dir: str):
        """Recurse over objects in a subdirectory of `dirname`"""
        for _obj in listdir(_dir):
            if os.path.isdir(_obj):
                recurse(_obj)
            elif _dir != dirname:
                _move(_obj, dirname)

    # Recursively move all nested files to top directory `dirname`
    recurse(dirname)

    # Remove all sub-directories
    for obj in listdir(dirname):
        if os.path.isdir(obj):
            shutil.rmtree(obj)

def _move(obj: str, to_dir: str):
    """Move a file from location `obj` to directory `to_dir`."""
    obj_name = os.path.split(obj)[-1]
    shutil.move(obj, os.path.join(to_dir, obj_name))

def get_mount_point(dirname: str) -> str:
    """Get the file system mount point for a directory.

    Args:
        dirname: Path to directory.

    Returns:
        Directory path to device mount point.
    """
    path = os.path.abspath(dirname)
    while not os.path.ismount(path):
        path = os.path.dirname(path)
    return path

def gzip_file(path: str) -> str:
    """Compress a file with gzip.

    Args:
        path: File to be compressed.

    Returns:
        Path of compressed file.
    """
    outfile = path + ".gz"
    with open(path, "rb") as source, gzip.open(outfile, "wb") as sink:
        shutil.copyfileobj(source, sink)

    return outfile

def gunzip_file(path: str) -> str:
    """Uncompress a gzipped file.

    Args:
        path: File path to a gzipped file.

    Returns:
        Path to unzipped file.
    """
    gzip_exts = {".gz", ".gzip"}
    if os.path.splitext(path)[-1] not in gzip_exts:
        raise IOError(f"{path} does not appear to be a gzipped file")

    outfile = os.path.splitext(path)[0]
    with gzip.open(path, "rb") as source, open(outfile, "wb") as sink:
        shutil.copyfileobj(source, sink)

    return outfile

def tar_gzip_files(tar_name: str, *file_paths: str):
    """Create a tarball of files and gzip them.

    Args:
        tar_name: Name of resulting .tar.gz file.
        *file_paths: Paths to files that will be included in tarball.
    """
    # Make all files relative to each other
    tar_path = os.path.realpath(tar_name)
    real_paths = [os.path.realpath(f) for f in file_paths]
    prefix = os.path.commonprefix(real_paths + [tar_path])
    tar_path = ltrim(tar_path, prefix)
    real_paths = [ltrim(f, prefix) for f in real_paths]

    # Work locally from the common relative to prevent bad tarballs
    orig_path = os.getcwd()
    os.chdir(prefix)
    with tarfile.open(tar_path, "w:gz") as tar:
        for file in real_paths:
            tar.add(file)
    os.chdir(orig_path)

def tar_gunzip_file(path: str) -> List[str]:
    """Uncompress a .tar.gz archive.

    Args:
        path: Path to .tar.gz file.

    Returns:
        List of files which were uncompressed.
    """
    tar_ext = ".tar.gz"
    if not path.endswith(tar_ext):
        raise IOError(f"{path} does not appear to be a tar archive")

    # Extract tar files into the same directory that the tarball is in
    dirname = os.path.dirname(os.path.realpath(path))
    with tarfile.open(path, "r:gz") as tar:
        files = [os.path.join(dirname, f) for f in tar.getnames()]
        tar.extractall(dirname)

    return files

def unzip_all(dirname: str) -> List[str]:
    """Unzip all *.zip files in a directory.

    Args:
        dirname: A directory to search for zipped files at top level.

    Returns:
        A list of all files which were unzipped.
    """
    abspath = os.path.abspath(dirname)
    files = [
        os.path.join(abspath, x)
        for x in os.listdir(abspath)
        if os.path.splitext(x)[-1] == ".zip"
    ]
    unzipped = list()
    for file in files:
        with zipfile.ZipFile(file, "r") as f:
            members = [x for x in f.namelist() if not x.endswith(os.path.sep)]
            f.extractall(abspath)
        unzipped += members

    return unzipped

def zip_dir(dirname: str, remove: bool = False) -> str:
    """Zip a directory.

    Args:
        dirname: A directory to zip.
        remove: If specified, will remove the directory after zipping.

    Returns:
        Path to created zipfile.
    """
    abspath = os.path.abspath(dirname)
    parent = os.path.dirname(abspath)
    zipname = f"{abspath}.zip"

    with zipfile.ZipFile(zipname, "w", zipfile.ZIP_DEFLATED) as f:
        files = [
            os.path.join(root, file)
            for root, _, files in os.walk(abspath)
            for file in files
        ]
        for file in files:
            arcname = file.replace(parent, "").lstrip(os.path.sep)
            f.write(file, arcname)

    if remove:
        shutil.rmtree(abspath)

    return zipname

def hash_file(filepath: str) -> str:
    """Create md5 hash of a file

    Args:
        filepath: Path to file that will be hashed.

    Returns:
        String of hex hash.
    """
    hash_obj = hashlib.md5()
    with open(filepath, "rb", buffering=0) as file:
        for byte_string in iter(lambda: file.read(128 * 1024), b""):
            hash_obj.update(byte_string)
    return hash_obj.hexdigest()

def stream_file_in(
    filepath: str, chunk_size: int = BUFFER
) -> Generator[bytes, None, None]:
    """Stream the contents of a file into memory in chunks.

    Args:
        filepath: Path of file to read.
        chunk_size: Maximum size of each chunk.

    Returns:
        Generator of bytes chunks.
    """
    with open(filepath, "rb") as buffer:
        for chunk in stream_buffer_in(buffer, chunk_size):
            yield chunk

def stream_buffer_in(
    buffer: BinaryIO, chunk_size: int = BUFFER
) -> Generator[bytes, None, None]:
    """Stream the contents of a buffer into memory in chunks.

    Args:
        buffer: Read buffer.
        chunk_size: Maximum size of each chunk.

    Returns:
        Generator of bytes chunks.
    """

    while True:
        chunk = buffer.read(chunk_size)

        if not chunk:
            break

        yield chunk

def stream_file_out(filepath: str) -> Generator[None, bytes, None]:
    """Create an output stream to a file.

        Used to avoid context managers for complex stream operations.

    Args:
        filepath: Path to stream output file.

    Returns:
        Generator which takes bytes and writes to a file.
    """
    maybe_makedirs(os.path.dirname(filepath))

    def _make_init():
        with open(filepath, "wb") as sink:
            while True:
                part = yield
                sink.write(part)

    gen = _make_init()
    next(gen)
    return gen

def chunk_file(filepath: str, chunk_size: int = 1_000_000_000) -> List[str]:
    """Divide a file into chunks of maximum `chunk_size` bytes each.

        If a file such as "large.csv" is passed to this function, then the
        files ["large.csv._00004_00000", ..., "large.csv._00004_00003"]
        will be produced, following the underscore and 5 digits pattern, where
        the first 5 digits are the total number of files and the next 5 digits
        are the part number.

    Args:
        filepath: Path to divide into chunks.
        chunk_size: Maximum size for each chunk in bytes.

    Returns:
        List of chunk file paths created by this function.
    """
    # Calculate number of chunks to be created - this is a function of the
    # buffer size in which the file will be streamed in, not just chunk_size
    total_chunks = math.ceil(
        math.ceil(os.path.getsize(filepath) / BUFFER) * BUFFER / chunk_size
    )

    # List to hold filenames of all chunks created
    chunk_files = list()

    # State variables to monitor which chunk we are on
    chunk = 0
    current_size = 0
    current_file = f"{filepath}._{total_chunks:05d}_{chunk:05d}"
    chunk_files.append(current_file)

    # Stream in file and divide into chunks
    sink = stream_file_out(current_file)
    for part in stream_file_in(filepath):
        part_size = len(part)

        # If this part will exceed the chunk limit, make a new chunk
        if (current_size + part_size) > chunk_size:
            chunk += 1
            current_size = 0
            current_file = f"{filepath}._{total_chunks:05d}_{chunk:05d}"
            chunk_files.append(current_file)

            sink.close()
            sink = stream_file_out(current_file)

        sink.send(part)
        current_size += part_size

    sink.close()
    return chunk_files

def rebuild_file(target_file: str):
    """Rebuilds a chunked file created by `chunk_file`.

    Args:
        target_file: The output file name.
    """
    chunk_files = find_target_chunks(target_file)

    with open(target_file, "wb") as sink:
        for file in chunk_files:
            for part in stream_file_in(file):
                sink.write(part)

def find_target_chunks(target_file: str) -> List[str]:
    """Given a base filename, returns are chunks matching that file pattern.

    Args:
        target_file: The output file name to find parts for.

    Returns:
        List of parts files.
    """
    dirname, filename = os.path.split(os.path.realpath(target_file))

    pattern = re.escape(filename) + CHUNK_PATTERN
    part_files = sorted(
        [os.path.join(dirname, f) for f in os.listdir(dirname) if re.search(pattern, f)]
    )
    return part_files

def rebuild_all_chunks(dirname: str) -> List[str]:
    """Rebuild all chunked files in a directory.

    Args:
        dirname: Path to directory where chunked files may exist.

    Returns:
        List of all files which were rebuilt.
    """
    # Get basename of each chunked file
    chunked_files = {chunk_basename(file) for file in find_all_chunks(dirname)}

    for file in chunked_files:
        rebuild_file(os.path.join(dirname, file))

    return list(chunked_files)

def find_all_chunks(dirname: str) -> List[str]:
    """Return all files in a directory matching the `CHUNK_PATTERN` regexp.

    Args:
        dirname: Path to directory where chunked files may exist.

    Returns:
        List of files which are chunks.
    """
    dirname = os.path.realpath(dirname)
    chunks = [
        os.path.join(dirname, file) for file in os.listdir(dirname) if is_chunk(file)
    ]
    return chunks

def is_chunk(filename: str) -> bool:
    """Determine if a filename is a chunk.

    Args:
        filename: A file name.

    Returns:
        True if matches the `CHUNK_PATTERN` regexp.
    """
    return bool(re.search(CHUNK_PATTERN, filename))

def chunk_basename(filename: str) -> str:
    """Given a filename with a chunk extension, return the original filename.

    Args:
        filename: Filename with chunk extension.

    Returns:
        Original filename. Example: "file.csv".
    """
    return re.sub(CHUNK_PATTERN, "", filename)

def blob_type_from_file(filename: str) -> Optional[BlobType]:
    """Given a filename, return the datamodel.BlobType

    Args:
        filename: Filename with chunk extension.

    Returns:
        BlobType object
    """
    if filename.endswith(".jfp"):
        return BlobType.MODELDB
    if filename.endswith(".step"):
        return BlobType.CAD
    elif filename.endswith(".csv"):
        return BlobType.CSV
    elif filename.endswith(".py"):
        return BlobType.SIMAPI
    elif filename.endswith(".bincad"):
        return BlobType.BINCAD
    elif filename.endswith(".brep"):
        return BlobType.BREP
    else:
        return None


# Need more info? Check out the blogpost:
# how-to-make-a-bot-that-automatically-replies-to-comments-on-facebook-post

"""
NEED MORE INFO? CHECK OUT THE BLOGPOST
https://vandevliet.me/bot-automatically-responds-comments-facebook/
"""

import sqlite3
from time import sleep
import facebook
from PIL import Image
from io import BytesIO

APP_ID = 'TO FILL IN'
APP_SECRET = 'TO FILL IN'

PAGE_ID = 'TO FILL IN'
POST_ID_TO_MONITOR = 'TO FILL IN'
LONG_LIVED_ACCESS_TOKEN = 'TO FILL IN'

COMBINED_POST_ID_TO_MONITOR = '%s_%s' % (PAGE_ID, POST_ID_TO_MONITOR)

def make_new_profile_pic(img):
    im = Image.open(BytesIO(img))
    im = im.resize((480, 480))

    # background version
    # background = Image.open("./fcdk_overlay.png")
    #
    # background.paste(im, (100, 100))

    # background.show()
    # bytes_array = BytesIO()
    # background.save(bytes_array, format='PNG')
    # bytes_array = bytes_array.getvalue()
    # return bytes_array

    # foreground version
    foreground = Image.open("./tgr_overlay.png")
    foreground = foreground.resize((250, 250))
    im = im.resize((250, 250))

    im.paste(foreground, (0, 0), foreground)

    # im.show()
    bytes_array = BytesIO()
    im.save(bytes_array, format='PNG')
    bytes_array = bytes_array.getvalue()
    return bytes_array

def comment_on_comment(graph, comment):
    comment_id = comment['id']
    comment_from_name = comment['from']['name']
    comment_from_id = comment['from']['id']
    comment_message = comment['message']
    profile = None

    print("Let's comment!")
    # like the comment
    graph.put_like(object_id=comment_id)

    # profile info:
    photo = graph.get_connections(id=comment_from_id, connection_name='picture', height=480, width=480)
    # try to get first name, if it's a page there is not first_name
    try:
        profile = graph.get_object(comment_from_id, fields='first_name,last_name')
    except:
        pass

    # let's create our photo that we want to post
    photo_to_post = make_new_profile_pic(photo['data'])

    # first we upload it as an unpublished photo that doesn't appear as a story (in the newsfeed)
    posted_photo = graph.put_photo(
        image=photo_to_post,
        album_path='404896546596495/photos',
        no_story=True,
        published=False
    )

    # if it's a person that commented, we can use the first name
    if profile:
        graph.put_object(parent_object=comment_id, connection_name='comments',
                         message='Bozour %s. Hier is uwe foto, mijn gedacht!' % (profile['first_name']),
                         attachment_id=posted_photo['id']
                         )
    else:
        graph.put_object(parent_object=comment_id, connection_name='comments',
                         message='Hola copain. Hier is uwe foto, mijn gedacht!',
                         attachment_id=posted_photo['id']
                         )

    print('Edited and posted the photo of: %s' % comment_from_name)

def monitor_fb_comments():
    # create graph
    graph = facebook.GraphAPI(LONG_LIVED_ACCESS_TOKEN)
    # that infinite loop tho
    while True:
        print('I spy with my little eye...🕵 ')
        sleep(5)

        # get the comments
        comments = graph.get_connections(COMBINED_POST_ID_TO_MONITOR,
                                         'comments',
                                         order='chronological')

        for comment in comments['data']:

            # if we can't find it in our comments database, it means
            # we haven't commented on it yet
            if not Posts().get(comment['id']):
                comment_on_comment(graph, comment)

                # add it to the database, so we don't comment on it again
                Posts().add(comment['id'])

        # while there is a paging key in the comments, let's loop them and do exactly the same
        # if you have a better way to do this, PRs are welcome :)
        while 'paging' in comments:
            comments = graph.get_connections(COMBINED_POST_ID_TO_MONITOR,
                                             'comments',
                                             after=comments['paging']['cursors']['after'],
                                             order='chronological')

            for comment in comments['data']:

                if not Posts().get(comment['id']):
                    comment_on_comment(graph, comment)
                    Posts().add(comment['id'])

# Mary had a little class
class Posts:
    def __init__(self):
        self.connection = sqlite3.connect('comments.sqlite3')
        self.cursor = self.connection.cursor()

    def get(self, id):
        self.cursor.execute("SELECT * FROM comments where id='%s'" % id)

        row = self.cursor.fetchone()

        return row

    def add(self, id):
        try:
            self.cursor.execute("INSERT INTO comments VALUES('%s')" % id)
            lid = self.cursor.lastrowid
            self.connection.commit()
            return lid
        except sqlite3.IntegrityError:
            return False

# started at the bottom, etc
if __name__ == '__main__':
    monitor_fb_comments()


import PyPDF2
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
import re

with open('sample.pdf','rb') as pdf_file, open('sample.txt', 'w') as text_file:
    read_pdf = PyPDF2.PdfFileReader(pdf_file)
    number_of_pages = read_pdf.getNumPages()
    for page_number in range(number_of_pages):   # use xrange in Py2
        page = read_pdf.getPage(page_number)
        page_content = page.extractText()
        text_file.write(page_content)

with open('sample.txt', 'r', encoding="latin-1") as f:
    read_output = f.readlines()
read_output = '\n'.join(read_output).strip().replace('\n','')

REPLACE_BY_SPACE_RE = re.compile('[/(){}\[\]\|@,;.]')
BAD_SYMBOLS_RE = re.compile('[^0-9a-z #+_]')
STOPWORDS = set(stopwords.words('english'))
SHORTWORD = re.compile(r'\W*\b\w{1,3}\b')

stop_words_lst = ['cell','spectral','data','double','bond','barrier','soluble', 'cells', 'performed using']

def clean_text(text):
    """
        text: a string

        return: modified initial string
    """
    text = str(text).lower() # lowercase text
    text = REPLACE_BY_SPACE_RE.sub(' ', text) # replace REPLACE_BY_SPACE_RE symbols by space in text. substitute the matched string in REPLACE_BY_SPACE_RE with space.
    text = BAD_SYMBOLS_RE.sub('', text) # remove symbols which are in BAD_SYMBOLS_RE from text. substitute the matched string in BAD_SYMBOLS_RE with nothing.
    text = re.sub(r'\b[a-zA-Z]\b','',str(text)) # remove single letter words
    text = re.sub('\d+','',text) # remove digits
    text = SHORTWORD.sub('',text)
    text = ' '.join(word for word in text.split() if word not in STOPWORDS) # remove stopwors from text
    for w in stop_words_lst:
        pattern = r'\b'+w+r'\b'
        text = re.sub(pattern, '', str(text))

    return text

pdf_o = clean_text(read_output)
text_tokens = word_tokenize(pdf_o)

pdf_i=[pdf_o]

# Getting bigrams
vectorizer = CountVectorizer(ngram_range =(2, 3))
X1 = vectorizer.fit_transform(pdf_i)
features = (vectorizer.get_feature_names())

X2 = vectorizer.fit_transform(pdf_i)
scores = (X2.toarray())

sums = X2.sum(axis = 0)
data1 = []
for col, term in enumerate(features):
    data1.append( (term, sums[0, col] ))
ranking = pd.DataFrame(data1, columns = ['term', 'rank'])
words = (ranking.sort_values('rank', ascending = False))
print ("\n\nWords : \n", words.head(50))


from Tools.Vector2D import Vector2D
import pygame

class SimpleObject(object):
    def __init__(self, role_image, position):
        self.position = position
        self.speed = 1
        self.role_image = pygame.image.load(role_image)
        self.width = self.role_image.get_width()
        self.height = self.role_image.get_height()

    def move_to(self, destination):
        direction = Vector2D.from_to(self.position, destination).normalize()
        x = self.position[0]
        if 0 < x < self.world.width:
            x = self.position[0] + direction.x * self.speed
        y = self.position[1]
        if 0 < y < self.world.height:
            y = self.position[1] + direction.y * self.speed
        self.position = (x, y)


import cv2
import numpy as np
from skimage import transform as trans

def face_align(img_rgb, landmarks):
    """
    This function aligns the face based on the 5 facial landmarks.

    :param img_rgb:
    :type img_rgb: np.ndarray
    :param landmarks:
    :type landmarks: list
    :return:
    """

    # These are predefined coordinates for 5 facial landmarks based on the standard 112x112 image.
    src = np.array([
        [30.2946, 51.6963],
        [65.5318, 51.5014],
        [48.0252, 71.7366],
        [33.5493, 92.3655],
        [62.7299, 92.2041]], dtype=np.float32)

    # Convert landmarks to numpy array and reshape it to (5, 2) for further operations.
    dst = np.array(landmarks, dtype=np.float32).reshape(5, 2)

    # Use similarity transform from skimage library to estimate the transformation matrix.
    tform = trans.SimilarityTransform()
    tform.estimate(dst, src)

    # Extract the transformation matrix from the transformation object.
    M = tform.params[0:2, :]

    # Apply the transformation matrix to the image to align the face.
    # Here, borderValue=0 is used to fill pixels outside the input boundaries.
    aligned = cv2.warpAffine(img_rgb, M, (112, 112), borderValue=0)

    return aligned


import random
import pygame
import math
from dataclasses import dataclass

SIZE = (400, 400)
MAX_LINE_LENGTH = 80

SPEED_MU = 20
MAX_SIZE = 3
WHITE = (255, 255, 255, 255)

@dataclass
class Star:
    pos: pygame.Vector2
    speed: pygame.Vector2
    size: int

    def update(self, screen, elapsed):
        pygame.draw.circle(screen, WHITE, self.pos, self.size)
        self.pos += elapsed * self.speed
        return int(self.pos.x) in range(SIZE[0]) and int(self.pos.y) in range(SIZE[1])

    def __sub__(self, other):
        return (self.pos - other.pos).length()

def map_distance_to_color(distance):
    if distance < MAX_LINE_LENGTH:
        g = int(255 * (1 - distance / MAX_LINE_LENGTH))
        return (g, g, g, g)
    return None

class StarField:

    def __init__(self, count=100):
        self._count = count
        self._stars = []
        self._respawn()

    def _respawn(self):
        for _ in range(self._count - len(self._stars)):
            star = Star(
                pos=pygame.Vector2(
                    random.randint(0, SIZE[0] - 1),
                    random.randint(0, SIZE[1] - 1),
                ),
                speed=pygame.Vector2(
                    random.gauss(0, SPEED_MU),
                    random.gauss(0, SPEED_MU),
                ),
                size=random.randint(1, MAX_SIZE),
            )
            self._stars.append(star)

    def update(self, screen, elapsed):
        remove = []
        lines = []
        for a in self._stars:
            for b in self._stars:
                if a is not b:
                    distance = a - b
                    color = map_distance_to_color(distance)
                    if color is not None:
                        lines.append((color, a.pos, b.pos))

        for color, a_pos, b_pos in sorted(lines, key=lambda line: line[0]):
            pygame.draw.line(screen, color, a_pos, b_pos)

        for star in self._stars:
            alive = star.update(screen, elapsed)
            if not alive:
                remove.append(star)
        for star in remove:
            self._stars.remove(star)
        self._respawn()

def main():
    pygame.init()
    pygame.display.set_caption("Star Geometry")

    screen = pygame.display.set_mode(SIZE)
    clock = pygame.time.Clock()
    star_field = StarField()

    running = True
    elapsed = 0

    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
        screen.fill((0, 0, 0))
        star_field.update(screen, elapsed / 1000)
        pygame.display.flip()
        elapsed = clock.tick(60)

if __name__ == "__main__":
    main()


from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qtagg import (
    FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.figure import Figure
from matplotlib import pyplot as plt
# TODO
# edit transactions

from PyQt5 import QtCore, Qt, QtGui
import numpy as np
import sys
import json
import traceback
import os
import datetime
import re
import requests
import random
import socket
sys.path.append("src")
from typing import Union
from building import *
from constants import *
from data import *
from transaction import Transaction, TransactionType
from buildings_tab import BuildingsTab

MY_VERSION = "1.3.5"

# really bad idea tbh
# try to guess the location of economy.json
# by looking at our cwd. if we're still in src
# then it's in ..
# else, assume it's in .
if os.path.basename(os.getcwd()) == "src":
    BACKUP_DIR = os.path.join("..", "backups")
    ECONOMY_FILE = os.path.join("..", "economy.json")
else:
    BACKUP_DIR = "backups"
    ECONOMY_FILE = "economy.json"

class Data:
    """
    Represents the economy.json file in an easier to work with way.
    Also handles the serialisation/deserialisation of all objects.
    This involves decoding the json and then constructing various
    `Building`, `Transaction` and `Loan` objects from the resulting dict
    """
    def __init__(self):
        self.regions = {}
        self.transactions = []
        self.current_day = None
        self.loans = []
        self.given_loans = []
        self.eco_cache = 0
        self.future_packets = []
        self.whoami = None

    def set_defaults(self):
        self.transactions.append(Transaction(TransactionType.MANUAL, datetime.date(2022, 10, 10).isoformat(), amount=40000, comment="Initial balance"))
        self.current_day = datetime.date(2022, 10, 10)

    def read_from_file(self, fname):
        with open(fname, "r") as f:
            raw_data = json.loads(f.read())

        self.current_day = datetime.date.fromisoformat(raw_data["current_day"])

        for reg in raw_data["regions"]:
            self.regions[reg] = []
            for b in raw_data["regions"][reg]["buildings"]:
                self.regions[reg].append(self.deserialise_building(b))

        self.transactions = [self.deserialise_transaction(t) for t in raw_data["transactions"]]
        self.loans = [self.deserialise_loan(l) for l in raw_data.get("loans", [])]
        self.given_loans = [self.deserialise_loan(l) for l in raw_data.get("given_loans", [])]
        self.future_packets = raw_data.get("future_packets", [])
        self.eco_cache = calc_income(self)[0]
        if raw_data.get("whoami"):
            self.whoami = raw_data["whoami"]
        else:
            self.whoami, entered = QtWidgets.QInputDialog.getText(None, "Select country", "Enter which country you are (for network communication)")
            if not entered:
                sys.exit(0)

    def write_to_file(self, fname):
        raw_data = {"current_day": self.current_day.isoformat(),
                    "regions": {r: {"buildings": [self.serialise_building(b) for b in self.regions[r]]} for r in self.regions},
                    "loans": [self.serialise_loan(l) for l in self.loans],
                    "given_loans": [self.serialise_loan(l) for l in self.given_loans],
                    "future_packets": self.future_packets,
                    "transactions": [self.serialise_transaction(t) for t in self.transactions]}
        # TODO in final version save whoami

        with open(fname, "w") as f:
            f.write(json.dumps(raw_data))

    def serialise_building(self, b):
        # either [type, size, lorentz] if only one
        # or [type, size, lorentz, count] if run length encoded
        if b.count == 1:
            return [b.btype, b.size, b.lorentz]
        else:
            return [b.btype, b.size, b.lorentz, b.count]

    def deserialise_building(self, obj, lorentz: float=None):
        if lorentz is None:
            lorentz = 1
        # old serialised buildings are either a list of [type, size]
        # or just a single int type. New serialised buildings are always
        # a list of [type, size, lorentz] to avoid ambiguity.
        # this is actually a lie now, *new* new buildings are either a
        # [type, size, lorentz] or a [type, size, lorentz, count]
        # for run length encoding
        if type(obj) == list:
            if len(obj) == 2: # old building, type and size
                return Building(obj[0], self.current_day, lorentz, obj[1])
            elif len(obj) == 3: # new building, type size and lorentz
                return Building(obj[0], self.current_day, obj[2], obj[1])
            elif len(obj) == 4: # new new buildig, (type, size, lorentz, count)
                return Building(obj[0], self.current_day, obj[2]. obj[1], count=obj[3])
        else: # old building, just type
            return Building(obj, self.current_day, lorentz)

    def serialise_transaction(self, trans):
        if trans.trans_type in (TransactionType.MANUAL, TransactionType.TAKEN_LOAN, TransactionType.GIVEN_LOAN):
            return {"amount": trans.amount,
                    "comment": trans.comment,
                    "type": trans.trans_type,
                    "timestamp": trans.timestamp}
        else:
            return {"buildings": [self.serialise_building(b) for b in trans.buildings],
                    "type": trans.trans_type,
                    "timestamp": trans.timestamp}

    def deserialise_transaction(self, object):
        if object["type"] in (TransactionType.MANUAL, TransactionType.TAKEN_LOAN, TransactionType.GIVEN_LOAN):
            return Transaction(object["type"], object["timestamp"], amount=object["amount"], comment=object["comment"])
        else:
            if object.get("buildings") == None: # old transaction, assume one building + count (+ lorentz)
                buildings = [self.deserialise_building(object["building"], lorentz=object.get("lorentz"))] * object["count"]
                return Transaction(object["type"], object["timestamp"], buildings=buildings)
            else: # new transaction, deserialise list of buildings with one lorentz each
                return Transaction(object["type"], object["timestamp"], buildings=[self.deserialise_building(i) for i in object["buildings"]])

    def serialise_loan(self, loan):
        return [loan.amount, loan.interest_rate, loan.country_name, loan.amount_paid, loan.uid]

    def deserialise_loan(self, obj):
        return Loan(obj[0], obj[1], obj[2], obj[3], obj[4] if len(obj) > 4 else None)

    def save(self):
        self.write_to_file(ECONOMY_FILE)

    def add_region(self, reg_name):
        self.regions[reg_name] = []

    def remove_region(self, reg_name):
        del self.regions[reg_name]

class Loan:
    def __init__(self, amount, interest_rate, country_name, amount_paid, uid=None):
        self.amount = amount
        self.interest_rate = interest_rate
        self.country_name = country_name
        self.amount_paid = amount_paid
        if uid is None:
            self.uid = random.randint(0, 2**31-1)
        else:
            self.uid = uid

def update_backup_formats():
    """Load and save every backup and the economy file,
       which should save every file in the latest format"""

    if not os.path.isdir(BACKUP_DIR):
        return

    for fname in os.listdir(BACKUP_DIR):
        newdata = Data()
        newdata.read_from_file(os.path.join(BACKUP_DIR, fname))
        newdata.write_to_file(os.path.join(BACKUP_DIR, fname))

    newdata = Data()
    newdata.read_from_file(ECONOMY_FILE)
    newdata.write_to_file(ECONOMY_FILE)

def get_historical_datas(data):
    """Return a list of backups, plus the current data"""
    if not os.path.isdir(BACKUP_DIR):
        return []

    datas = []
    for fname in os.listdir(BACKUP_DIR):
        newdata = Data()
        newdata.read_from_file(os.path.join(BACKUP_DIR, fname))
        datas.append(newdata)

    datas.append(data)
    return datas

def send_info_popup(txt):
    """Show an info messagebox"""
    msg = QtWidgets.QMessageBox()
    msg.setIcon(QtWidgets.QMessageBox.Information)
    msg.setText(txt)
    msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
    msg.exec_()

class KeybindTable(QtWidgets.QTableWidget):
    """Wrapper around a QTableWidget to expose key press events.
    Needed for detecting the delete key to delete a transaction"""
    keyPressed = Qt.pyqtSignal(QtGui.QKeyEvent)
    def keyPressEvent(self, event):
        if type(event) == QtGui.QKeyEvent:
            self.keyPressed.emit(event)

class TransactionsTab(QtWidgets.QWidget):
    recalculate = Qt.pyqtSignal()
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.parent = parent
        self.data = data
        self.layout = QtWidgets.QGridLayout(self)
        self.bottom_layout = QtWidgets.QHBoxLayout()

        self.table = KeybindTable(self)
        self.table.setColumnCount(3)
        self.table.setRowCount(0)
        self.table.setHorizontalHeaderLabels(["Amount", "Date", "Comment"])
        header = self.table.horizontalHeader()
        header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)

        self.e_amount = QtWidgets.QLineEdit(self)
        self.e_comment= QtWidgets.QLineEdit(self)
        self.b_add    = QtWidgets.QPushButton("Add", self)

        self.e_amount.setPlaceholderText("Amount")
        self.e_comment.setPlaceholderText("Comment")

        self.layout.addWidget(self.table, 1, 0, 1, 3)
        self.layout.addWidget(self.e_amount, 0, 0)
        self.layout.addWidget(self.e_comment, 0, 1)
        self.layout.addWidget(self.b_add, 0, 2)
        self.layout.setColumnStretch(1, 1)
        self.setLayout(self.layout)

        self.b_add.clicked.connect(self._add_transaction_button)
        self.table.keyPressed[QtGui.QKeyEvent].connect(self._table_keypress)

        self.transaction_widgets = []

        for t in data.transactions:
            self._add_transaction_to_table(t)
        self.recalculate.emit()

    def _table_keypress(self, event):
        if event.key() == QtCore.Qt.Key_Delete and self.table.rowCount() > 0:
            row = self.table.currentRow()
            t = self.data.transactions[row]
            if t.trans_type != TransactionType.MANUAL:
                pass#return

            cont = QtWidgets.QMessageBox.question(self, "Really delete transaction?", "Really delete transaction?")
            if cont == QtWidgets.QMessageBox.No:
                return
            self.data.transactions.pop(row)
            self.table.removeRow(row)
            self.data.save()
            self.recalculate.emit()

    def _add_transaction_button(self):
        """Add a manual transaction"""
        try:
            amount = round(float(self.e_amount.text()), 2)
        except ValueError:
            send_info_popup("Enter a valid number for the amount (without any $)")
            return

        date = self.data.current_day.isoformat()
        comment = self.e_comment.text()
        self.add_transaction(Transaction(TransactionType.MANUAL, date, comment=comment, amount=amount))
        self.e_comment.setText("")
        self.e_amount.setText("")

    def add_transaction(self, transaction: Transaction):
        self.data.transactions.append(transaction)
        self.data.save()

        self._add_transaction_to_table(transaction)
        self.recalculate.emit()

    def _add_transaction_to_table(self, transaction: Transaction):
        row = self.table.rowCount()
        self.table.setRowCount(row + 1)
        self.set_row_to(row, transaction)

    def set_row_to(self, row, transaction):
        self.table.setItem(row, 0, QtWidgets.QTableWidgetItem(format_money(transaction.compute_amount())))
        self.table.setItem(row, 1, QtWidgets.QTableWidgetItem(format_date(transaction.timestamp)))
        self.table.setItem(row, 2, QtWidgets.QTableWidgetItem(transaction.compute_comment()))

def calc_series(datas, series):
    """Return a list of datapoints calculated from the backups"""
    if series == "Balance":
        return [calc_bal(d) for d in datas]
    elif series == "Population":
        return [calc_population(d)[0] for d in datas]
    elif series == "Income":
        return [calc_income(d)[0] for d in datas]
    elif series == "Expenditure":
        vals = []
        for d in datas:
            vals.append(0)
            for trans in d.transactions:
                if trans.timestamp == d.current_day.isoformat() and trans.compute_amount() < 0:
                    vals[-1] -= trans.compute_amount()
        return vals
    elif series == "Employment":
        return [calc_employment(d) * 100 for d in datas]

    elif series == "Time":
        return [i for i, d in enumerate(datas)]

class GraphControls(QtWidgets.QWidget):
    """Left-hand side bar used to control the graph"""
    def __init__(self, figure, data, parent=None):
        super().__init__(parent)
        self.figure = figure
        self.data = data
        self.ax = figure.subplots()

        self.layout = QtWidgets.QVBoxLayout(self)
        self.graph_type = QtWidgets.QComboBox(self)
        self.x_axis = QtWidgets.QComboBox(self)
        self.y_axis = QtWidgets.QComboBox(self)

        self.l_gtype = QtWidgets.QLabel("Graph type", self)
        self.l_xaxis = QtWidgets.QLabel("Plot...", self)
        self.l_yaxis = QtWidgets.QLabel("Against...", self)

        self.b_plot = QtWidgets.QPushButton("Plot", self)
        self.b_clear = QtWidgets.QPushButton("Clear", self)

        self.graph_type.addItems(["Line graph", "Scatter graph", "Pie chart"])

        self.layout.addWidget(self.l_gtype)
        self.layout.addWidget(self.graph_type)
        self.layout.addWidget(self.l_xaxis)
        self.layout.addWidget(self.x_axis)
        self.layout.addWidget(self.l_yaxis)
        self.layout.addWidget(self.y_axis)
        self.layout.addWidget(self.b_plot)
        self.layout.addWidget(self.b_clear)

        self.setLayout(self.layout)

        self.graph_type.activated[str].connect(lambda x: self._update())
        self.x_axis.activated[str].connect(lambda x: self._update())
        self.y_axis.activated[str].connect(lambda x: self._update())
        self.b_plot.clicked.connect(self._plot)
        self.b_clear.clicked.connect(self._clear)

        self._update()

    def _clear(self):
        self.figure.clear()
        self.ax = self.figure.subplots()
        # self.ax.clear()
        self.figure.canvas.draw()

    def _update(self):
        ty = self.graph_type.currentText()
        itemx = self.x_axis.currentIndex()
        itemy = self.y_axis.currentIndex()
        self.x_axis.clear()
        self.y_axis.clear()
        # TODO graph something of just one region
        if ty == "Line graph" or ty == "Scatter graph":
            self.x_axis.addItems(["Balance", "Income", "Expenditure", "Employment", "Population"])
            self.y_axis.addItems(["Time"])

        if ty == "Scatter graph":
            self.y_axis.addItems(["Balance", "Income", "Expenditure", "Employment", "Population"])

        if ty == "Pie chart":
            self.x_axis.addItems(["Income", "Population"])
            self.y_axis.addItems(["Region", "Industry"])

        # elif ty == "Bar chart":
            # self.x_axis.addItems(["Employment"])
            # self.y_axis.addItems(["Region"])

        if itemx < self.x_axis.count():
            self.x_axis.setCurrentIndex(itemx)

        if itemy < self.y_axis.count():
            self.y_axis.setCurrentIndex(itemy)

    def _plot(self):
        """???"""
        # TODO do it
        gtype = self.graph_type.currentText()
        xaxis = self.x_axis.currentText()
        yaxis = self.y_axis.currentText()
        if len(xaxis) < 1 or len(yaxis) < 1:
            return

        if gtype == "Pie chart":
            if xaxis == "Income" and yaxis == "Region":
                _, regional_income = calc_income(data)
                values, labels = regional_income.values(), regional_income.keys()
            elif xaxis == "Population" and yaxis == "Region":
                _, regional_pop = calc_population(data)
                values, labels = regional_pop.values(), regional_pop.keys()
            elif xaxis == "Income" and yaxis == "Industry":
                ind = calc_industry_income(data)
                values, labels = ind.values(), ind.keys()
            else:
                return # invalid configuration

            values, labels = zip(*sorted(zip(values, labels), key=lambda i: i[0]))
            cm = plt.get_cmap("plasma")
            colours = [cm(i) for i in np.linspace(0, 1, len(values))]

            self.ax.pie(values, labels=labels, autopct='%1.1f%%', shadow=True, startangle=90, colors=colours)
            self.ax.axis('equal')

        elif gtype == "Line graph" or gtype == "Scatter graph":
            datas = sorted(get_historical_datas(self.data), key=lambda d: d.current_day)
            xvals = calc_series(datas, xaxis)
            yvals = calc_series(datas, yaxis)
            if gtype == "Line graph":
                self.ax.plot(yvals, xvals)
            else:
                self.ax.scatter(yvals, xvals)

        # elif gtype == "Bar chart":
            # if xaxis == "Employment" and yaxis == "Region":
                # _, regional_employment = calc_employment(data)
                # self.ax.bar(regional_employment.keys(), list(map(lambda n: n * 100, regional_employment.values())))

        self.figure.canvas.draw()

class MoronException(Exception):
    """For use if you make a file called `backups`"""
    pass

class StatsTab(QtWidgets.QWidget):
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.parent = parent
        self.layout = QtWidgets.QGridLayout(self)

        self.graph_layout = QtWidgets.QVBoxLayout()
        self.figure = Figure()
        self.canvas = FigureCanvas(self.figure)
        self.toolbar = NavigationToolbar(self.canvas, self)

        self.graph_layout.addWidget(self.toolbar)
        self.graph_layout.addWidget(self.canvas)

        self.graph_controls = GraphControls(self.figure, data, self)

        self.layout.addWidget(self.graph_controls, 0, 0, 1, 1)
        self.layout.addLayout(self.graph_layout, 0, 1, 3, 1)
        self.layout.setColumnStretch(1, 1)
        self.setLayout(self.layout)

class LoanList(QtWidgets.QFrame):
    """Represents a list of loans. Basically just a manually controlled table without the table"""
    payment_made = Qt.pyqtSignal(Loan)
    def __init__(self, label: str, allow_payment: bool, parent=None):
        super().__init__(parent)
        self.layout = QtWidgets.QGridLayout(self)
        self.allow_payment = allow_payment

        self.layout.addWidget(QtWidgets.QLabel(label, self), 0, 0, 1, 3, alignment=QtCore.Qt.AlignCenter)
        self.layout.addWidget(QtWidgets.QLabel("Amount due for payback", self), 1, 0)
        self.layout.addWidget(QtWidgets.QLabel("Interest rate", self), 1, 1)
        self.layout.addWidget(QtWidgets.QLabel("Name", self), 1, 2)
        self.layout.setRowStretch(1000, 1)

        self.setFrameStyle(QtWidgets.QFrame.Raised | QtWidgets.QFrame.StyledPanel)
        self.curr_row = 2
        self.loan_widgets = []
        self.setLayout(self.layout)

    def add_loan_widgets(self, loan):
        if self.allow_payment:
            self.loan_widgets.append((
                QtWidgets.QLabel(format_money(loan.amount)),
                QtWidgets.QLabel(str(round(loan.interest_rate, 2)) + "%"),
                QtWidgets.QLabel(loan.country_name),
                QtWidgets.QPushButton("Make payment")
            ))
            self.loan_widgets[-1][3].clicked.connect(lambda: self.payment_made.emit(loan))
        else:
            # don't include the "Make payment" button
            self.loan_widgets.append((
                QtWidgets.QLabel(format_money(loan.amount)),
                QtWidgets.QLabel(str(round(loan.interest_rate, 2)) + "%"),
                QtWidgets.QLabel(loan.country_name),
            ))

        for col, w in enumerate(self.loan_widgets[-1]):
            self.layout.addWidget(w, self.curr_row, col)

        self.curr_row += 1

    def clear(self):
        for row in self.loan_widgets:
            for w in row:
                self.layout.removeWidget(w)
        self.loan_widgets.clear()
        self.curr_row = 2

class LoansTab(QtWidgets.QWidget):
    loan_given = Qt.pyqtSignal(Loan)
    payment_made = Qt.pyqtSignal(Loan, float)
    un_loan_taken = Qt.pyqtSignal(float)

    def __init__(self, data, parent=None):
        super().__init__(parent)

        self.layout = QtWidgets.QGridLayout(self)

        self.e_amount = QtWidgets.QDoubleSpinBox(self)
        self.e_amount.setMaximum(999999999)
        self.l_amount = QtWidgets.QLabel("Amount", self)
        self.b_un = QtWidgets.QCheckBox("UN loan", self)
        self.e_interest_rate = QtWidgets.QDoubleSpinBox(self)
        self.l_interest_rate = QtWidgets.QLabel("Interest Rate (%)", self)
        self.e_name = QtWidgets.QLineEdit(self)
        self.l_name = QtWidgets.QLabel("Name", self)
        self.b_give_loan = QtWidgets.QPushButton("Give loan", self)

        self.given_loans_widget = LoanList("Loans given out", False, self)
        self.taken_loans_widget = LoanList("Loans taken out", True, self)

        self.layout.addWidget(self.l_amount, 0, 0)
        self.layout.addWidget(self.e_amount, 1, 0)
        self.layout.addWidget(self.b_un,     1, 1)
        self.layout.addWidget(self.l_interest_rate, 0, 2)
        self.layout.addWidget(self.e_interest_rate, 1, 2)
        self.layout.addWidget(self.l_name, 0, 3)
        self.layout.addWidget(self.e_name, 1, 3)
        self.layout.addWidget(self.b_give_loan, 1, 4)
        self.layout.addWidget(self.given_loans_widget, 2, 0, 1, 5)
        self.layout.addWidget(self.taken_loans_widget, 3, 0, 1, 5)
        self.layout.setRowStretch(2, 1)
        self.layout.setRowStretch(3, 1)

        self.setLayout(self.layout)

        self.b_un.clicked.connect(self._un_loan)
        self.b_give_loan.clicked.connect(self._give_loan)
        self.taken_loans_widget.payment_made.connect(self._make_payment)
        self.update_loan_widgets(data)

    def _un_loan(self):
        """Called when the UN loan checkbox changes state"""
        if self.b_un.isChecked():
            self.e_interest_rate.setEnabled(False)
            self.e_interest_rate.setValue(UN_LOAN_INTEREST * 100)
            self.e_name.setEnabled(False)
            self.e_name.setText("UN")
            self.b_give_loan.setText("Take loan")
        else:
            self.b_give_loan.setText("Give loan")
            self.e_interest_rate.setEnabled(True)
            self.e_name.setEnabled(True)
            self.e_name.setText("")

    def _give_loan(self):
        if self.b_un.isChecked():
            self.un_loan_taken.emit(self.e_amount.value())
            self.taken_loans_widget.add_loan_widgets(Loan(self.e_amount.value(), UN_LOAN_INTEREST * 100, "UN", 0))
        else:
            loan = Loan(self.e_amount.value(), self.e_interest_rate.value(), self.e_name.text(), 0)
            self.loan_given.emit(loan)
            self.given_loans_widget.add_loan_widgets(loan)

    def _make_payment(self, loan):
        amount, ok = QtWidgets.QInputDialog.getDouble(self, "Make loan payment", "How much would you like to pay?", 0, 1, loan.amount, 2)
        if not ok:
            return
        # this is stupid
        # idx = -1
        # for i, l in enumerate(data.loans):
        #     if l[1] == loan[1] and l[2] == loan[2]:
        #         idx = i
        #         break
        # if idx == -1:
        #     raise MoronException("For some reason the loan you tried to pay off wasn't found in your total loan list. big bug report to jams plz")
        loan.amount_paid += amount
        loan.amount -= amount
        self.payment_made.emit(loan, amount)

    def update_loan_widgets(self, data):
        self.given_loans_widget.clear()
        self.taken_loans_widget.clear()
        for loan in data.given_loans:
            self.given_loans_widget.add_loan_widgets(loan)

        for loan in data.loans:
            self.taken_loans_widget.add_loan_widgets(loan)

class NetworkHandler:
    """receive, decode and handle network packets"""
    def __init__(self, data: Data, trans):
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.data = data
        self.trans = trans

    def read(self):
        data = b""
        while not b"\n" in data:
            d = self.s.recv(1024)
            if not d:
                break
            data += d
        print(data)
        data = json.loads(data.decode("utf-8"))
        return data

    def send(self, data):
        self.s.send(json.dumps(data).encode("utf-8") + b"\n")

    def __enter__(self):
        try:
            self.s.connect(("127.0.0.1", 7896))
        except Exception as e:
            send_info_popup("Error connecting to server: " + str(e))
            return None

        self.send({"whoami": data.whoami})
        packet_queue = self.read()
        for packet in packet_queue:
            date = datetime.date.fromisoformat(packet["date"])
            if date <= data.current_day:
                NetworkHandler.execute_packet(packet, self.data, self.trans, date)
            else:
                data.future_packets.append(packet)
        return self

    def execute_packet(packet, data: Data, trans, date: datetime.date):
        if packet["type"] == "give_loan":
            loan = data.deserialise_loan(packet["loan"])
            data.loans.append(loan)
            trans.add_transaction(Transaction(
                TransactionType.TAKEN_LOAN,
                date.isoformat(),
                comment=loan.country_name,
                amount=loan.amount
            ))
            data.save()
            send_info_popup(f"{loan.country_name} sent you a loan of {format_money(loan.amount)} at {loan.interest_rate:.2f}% interest")

    def __exit__(self, *args):
        self.send("exit")
        self.s.close()

class InfoBar(QtWidgets.QWidget):
    """Bottom bar of all tabs to show statistics"""
    update_day = Qt.pyqtSignal(int)
    refresh = Qt.pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)

        self.layout = QtWidgets.QVBoxLayout(self)
        self.stats_layout = QtWidgets.QGridLayout()
        self.date_layout = QtWidgets.QHBoxLayout()

        self.b_update_day = QtWidgets.QPushButton("Update day to today's date", self)
        self.b_next_day = QtWidgets.QPushButton("Next day", self)
        self.b_refresh = QtWidgets.QPushButton("Refresh network", self)
        self.b_update_day.clicked.connect(lambda: self.update_day.emit(-1))
        self.b_next_day.clicked.connect(lambda: self.update_day.emit(1))
        self.b_refresh.clicked.connect(self.refresh.emit)

        self.l_bal = QtWidgets.QLabel(self)
        self.l_income = QtWidgets.QLabel(self)
        self.l_employment = QtWidgets.QLabel(self)
        self.l_pop = QtWidgets.QLabel(self)
        self.l_jobs = QtWidgets.QLabel(self)
        self.l_lorentz = QtWidgets.QLabel(self)
        self.l_date = QtWidgets.QLabel(self)

        self.l_regincome = QtWidgets.QLabel(self)
        self.l_regpop = QtWidgets.QLabel(self)
        self.l_regjobs = QtWidgets.QLabel(self)
        self.l_regemploy = QtWidgets.QLabel(self)

        self.l_regional = QtWidgets.QLabel("Regional:", self)

        self.stats_layout.addWidget(self.l_regional,  0, 0)
        self.stats_layout.addWidget(self.l_regincome, 0, 2)
        self.stats_layout.addWidget(self.l_regemploy, 0, 3)
        self.stats_layout.addWidget(self.l_regpop,    0, 4)
        self.stats_layout.addWidget(self.l_regjobs,   0, 5)

        self.stats_layout.addWidget(QtWidgets.QLabel("National:", self), 1, 0)
        self.stats_layout.addWidget(self.l_bal,        1, 1)
        self.stats_layout.addWidget(self.l_income,     1, 2)
        self.stats_layout.addWidget(self.l_employment, 1, 3)
        self.stats_layout.addWidget(self.l_pop,        1, 4)
        self.stats_layout.addWidget(self.l_jobs,       1, 5)

        self.date_layout.addWidget(self.l_date)
        self.date_layout.addWidget(self.l_lorentz)
        self.date_layout.addWidget(self.b_refresh)
        self.date_layout.addWidget(self.b_next_day)
        self.date_layout.addWidget(self.b_update_day)

        self.layout.addLayout(self.stats_layout)
        self.layout.addLayout(self.date_layout)
        self.setLayout(self.layout)

    def _hide_regional(self):
        self.l_regional.hide()
        self.l_regincome.hide()
        self.l_regemploy.hide()
        self.l_regpop.hide()
        self.l_regjobs.hide()

    def _show_regional(self):
        self.l_regional.show()
        self.l_regincome.show()
        self.l_regemploy.show()
        self.l_regpop.show()
        self.l_regjobs.show()

    def update_info(self, data: Data, curr_region: str):
        pop, reg_pop = calc_population(data)
        jobs, reg_jobs = calc_jobs(data)
        income, regional_income = calc_income(data)
        bal = calc_bal(data)
        employment = calc_employment(data)

        data.eco_cache = income # TODO maybe bad idea?
        if curr_region != "Total":
            self._show_regional()
            pop_of_current_region = reg_pop[curr_region]
            jobs_of_current_region = reg_jobs[curr_region]

            if pop_of_current_region  != 0:
                employ_percent = jobs_of_current_region  / pop_of_current_region  * 100
            else:
                employ_percent = 0

            income_of_current_region  = regional_income[curr_region]

            self.l_regincome.setText("Income: " + str(format_money(income_of_current_region)))
            self.l_regemploy.setText("Employment: " + str(round(employ_percent, 1)) + "%")
            self.l_regpop.setText("Population: " + str(pop_of_current_region ))
            self.l_regjobs.setText("Jobs: " + str(round(jobs_of_current_region , 2)))
        else:
            # no regional stats to show
            self._hide_regional()

        self.l_income.setText("Income: " + format_money(income))
        self.l_employment.setText("Employment: " + str(round(employment * 100, 2)) + "%")
        self.l_pop.setText("Population: " + str(calc_population(data)[0]))
        self.l_jobs.setText("Jobs: " + str(round(calc_jobs(data)[0], 2)))

        self.l_lorentz.setText("L: " + str(round(Building.get_lorentz(data.eco_cache), 4)))
        self.l_date.setText("Current date: " + format_date(data.current_day.isoformat()))

        self.l_bal.setText("Balance: " + format_money(bal))

class Main(QtWidgets.QWidget):
    def __init__(self, data):
        super().__init__()
        self.data = data
        self.init_gui(data)

        self.show()

    def init_gui(self, data):
        self.layout = QtWidgets.QVBoxLayout(self)

        self.stats_tab = StatsTab(data, self)
        self.transactions_tab = TransactionsTab(data, self)
        self.buildings_tab = BuildingsTab(data, self)
        self.loans_tab = LoansTab(data, self)

        self.tab_widget = QtWidgets.QTabWidget(self)
        self.tab_widget.addTab(self.buildings_tab, "Buildings")
        self.tab_widget.addTab(self.transactions_tab, "Transactions")
        self.tab_widget.addTab(self.stats_tab, "Stats")
        self.tab_widget.addTab(self.loans_tab, "Loans")

        self.info_bar = InfoBar(self)

        self.layout.addWidget(self.tab_widget)
        self.layout.addWidget(self.info_bar)

        self.setLayout(self.layout)
        self.recalculate()
        self.transactions_tab.recalculate.connect(self.recalculate)
        self.info_bar.update_day.connect(lambda delta: self.update_day(delta if delta > 0 else None))
        self.info_bar.refresh.connect(self._refresh_network)
        self.buildings_tab.region_changed.connect(lambda region: self.info_bar.update_info(self.data, region))
        self.loans_tab.loan_given.connect(self.give_loan)
        self.loans_tab.un_loan_taken.connect(self.take_un_loan)
        self.loans_tab.payment_made.connect(self._loan_paid)

    def _refresh_network(self):
        # firstly check cached packets
        packets_to_remove = []
        for packet in self.data.future_packets:
            date = datetime.date.fromisoformat(packet["date"])
            if date <= data.current_day:
                NetworkHandler.execute_packet(packet, self.data, self.transactions_tab, date)
                packets_to_remove.append(packet)

        # there is probably a better way of doing this
        for packet in packets_to_remove:
            self.data.future_packets.remove(packet)

        with NetworkHandler(self.data, self.transactions_tab) as net:
            pass
        self.loans_tab.update_loan_widgets(self.data) # TODO inefficient

    def _loan_paid(self, loan, amount):
        if loan.amount < 0.01:
            self.data.loans.remove(loan)

        self.transactions_tab.add_transaction(Transaction(
            TransactionType.MANUAL,
            self.data.current_day.isoformat(),
            comment="Loan payment to " + loan.country_name,
            amount=-amount
        ))

        self.send_loan_payment_packet(loan, amount, self.data.current_day.isoformat())
        self.loans_tab.update_loan_widgets(self.data)

    def recalculate(self):
        self.info_bar.update_info(self.data, self.buildings_tab.curr_region)
        self.buildings_tab.recalc_preview()

    def take_un_loan(self, amount: float):
        self.data.loans.append(Loan(amount, UN_LOAN_INTEREST * 100, "UN", 0))
        self.transactions_tab.add_transaction(Transaction(
            TransactionType.TAKEN_LOAN,
            self.data.current_day.isoformat(),
            comment="UN",
            amount=amount,
        ))
        self.data.save()

    def give_loan(self, loan: Loan):
        self.data.given_loans.append(loan)
        self.transactions_tab.add_transaction(Transaction(
            TransactionType.GIVEN_LOAN,
            self.data.current_day.isoformat(),
            comment=loan.country_name,
            amount=loan.amount,
        ))
        self.send_loan_packet(loan, self.data.current_day.isoformat())
        self.data.save()

    def send_loan_payment_packet(self, loan: Loan, amount: float, date: str):
        with NetworkHandler(self.data, self.transactions_tab) as net:
            if not net: return
            net.send({"type": "loan_payment",
                      "player": loan.country_name,
                      "from": self.whoami,
                      "loan_uid": loan.uid,
                      "amount": amount,
                      "date": date})

    def send_loan_packet(self, loan: Loan, date: str):
        with NetworkHandler(self.data, self.transactions_tab) as net:
            if not net: return
            loan_ser = self.data.serialise_loan(loan)
            loan_ser[2] = self.data.whoami
            net.send({"type": "give_loan",
                      "player": loan.country_name,
                      "loan": loan_ser,
                      "date": date})

    def get_paid(self):
        # this check is currently redundant but I left it in for the lulz
        # actually that might not be true
        for n in data.transactions[::-1]:
            if n.comment == "Income" and n.timestamp == self.data.current_day.isoformat():
                send_info_popup("YE CANNAE FOCKEN DAE THAT M8\n(you can only get paid once per day)")
                return
        income, regional_income = calc_income(data)
        self.transactions_tab.add_transaction(Transaction(
            TransactionType.MANUAL,
            self.data.current_day.isoformat(),
            comment="Income",
            amount=income,
        ))
        bal = calc_bal(data)
        if bal < 0:
            self.transactions_tab.add_transaction(Transaction(
                TransactionType.MANUAL,
                self.data.current_day.isoformat(),
                comment="Overdraft interest",
                amount=bal * OVERDRAFT_INTEREST,
            ))

    def calc_loans(self):
        for loan in self.data.loans:
            loan.amount *= loan.interest_rate / 100 + 1
        self.loans_tab.update_loan_widgets(self.data)

    def update_day(self, delta=None):
        if delta is not None:
            now = datetime.date.today()
            next_day = self.data.current_day + datetime.timedelta(days=delta)
            if next_day > now:
                send_info_popup("Woah there buddy you aren't goint 88mph\n(you're trying to go into the future!)")
                return

        self.data.save()
        if os.path.exists(BACKUP_DIR) and os.path.isfile(BACKUP_DIR):
            raise MoronException("You absolute idiot, you made a file called 'backups', that's where I want to store my backups! Please delete or rename it")
        if not os.path.exists(BACKUP_DIR):
            os.mkdir(BACKUP_DIR)

        # Yes, this is a race condition or TOC/TOU bug
        # The truth is, I do not care, for it is exceedingly unlikely that anything could happen in between
        # also it wouldn't even matter that much it would just crash and save the progress anyway lmao

        self.data.write_to_file(os.path.join(BACKUP_DIR, self.data.current_day.isoformat() + ".json"))

        if delta is None:
            self.data.current_day = datetime.date.today()
        else:
            self.data.current_day += datetime.timedelta(days=delta)
        self.get_paid()
        self.calc_loans()
        self._refresh_network()
        self.recalculate()
        self.data.save()

def exception_hook(exctype, value, tb):
    data.save()
    exception_hook_no_save(exctype, value, tb)

def exception_hook_no_save(exctype, value, tb):
    traceback_formated = traceback.format_exception(exctype, value, tb)
    traceback_string = "".join(traceback_formated)
    print("Excepthook called, saving and quiteing...")
    print(traceback_string, file=sys.stderr)

    msg = QtWidgets.QMessageBox()
    msg.setIcon(QtWidgets.QMessageBox.Critical)
    msg.setText("An error occurred in the main process. Your data should be safe (in theory). Please send the following report to me(james):\n" + traceback_string)
    msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
    msg.exec_()
    sys.exit(1)

class Updater(QtWidgets.QDialog):
    def __init__(self):
        super().__init__()
        self.layout = QtWidgets.QVBoxLayout(self)
        self.doing = QtWidgets.QLabel(self)
        self.progress = QtWidgets.QProgressBar(self)
        self.layout.addWidget(self.doing)
        self.layout.addWidget(self.progress)
        self.setLayout(self.layout)

    def update(self):
        self.progress.setMinimum(0)
        self.progress.setMaximum(0)
        self.uworker = UpdateWorker(self)

        self.uworker.progress_changed.connect(lambda prog: self.progress.setValue(prog))
        self.uworker.progress_max.connect(lambda m: self.progress.setMaximum(m))
        self.uworker.update_status.connect(lambda s: self.doing.setText(s))
        self.uworker.result.connect(lambda res: self.update_done(res[0], res[1]))
        self.uworker.start()

    def update_done(self, updated, msg):
        if updated:
            send_info_popup(msg)
        elif msg is not None:
            choice = QtWidgets.QMessageBox.warning(self, "Update Failed",
                                   "Error updating: " + msg,
                                   QtWidgets.QMessageBox.Retry | QtWidgets.QMessageBox.Ignore,
                                   QtWidgets.QMessageBox.Retry)

            if choice == QtWidgets.QMessageBox.Retry:
                self.update()
                return
        self.done(updated)

class UpdateWorker(QtCore.QThread):
    """QThread that does the networking for updating"""
    progress_changed = Qt.pyqtSignal(int)
    progress_max = Qt.pyqtSignal(int)
    result = Qt.pyqtSignal(object)
    update_status = Qt.pyqtSignal(str)

    def __init__(self, parent):
        super().__init__()
        self.parent = parent

    def run(self):
        res = self.autoupdate()
        self.result.emit(res)

    def autoupdate(self):
        """Check for, and install updates. Returns (updated, message)"""
        self.update_status.emit("Checking for updates...")
        try:
            vers_r = requests.get("http://cospox.com/eco/version")
        except Exception as e:
            return False, "Getting version, other error " + str(e)
        if vers_r.status_code != 200:
            return False, "Getting version, status " + str(vers_r.status_code)
        if vers_r.text.strip() == MY_VERSION:
            # already up to date
            return False, None

        choice = QtWidgets.QMessageBox.question(self.parent,
                "Update available",
                "Version " + vers_r.text + " is available. Do you want to update?",
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes)
        if choice == QtWidgets.QMessageBox.No:
            # User selected not to update
            return False, None

        self.update_status.emit("Fetching file list...")

        try:
            flist = requests.get("http://cospox.com/eco/files")
        except Exception as e:
            return False, "Getting file list, other error " + str(e)

        if flist.status_code != 200:
            return False, "Getting file list, status " + str(flist.status_code)

        num_files = len(flist.text.strip().split(","))
        self.progress_max.emit(num_files + 1)
        val = 1
        self.progress_changed.emit(val)

        for fname in flist.text.strip().split(","):
            self.update_status.emit("Downloading " + fname + " (" + str(val) + "/" + str(num_files - 1) + ")")
            try:
                r = requests.get("http://cospox.com/eco/" + fname)
            except Exception as e:
                return False, "Getting file " + fname + ", other error " + str(e)

            if r.status_code != 200:
                return False, "Getting update file " + fname + ", status " + str(r.status_code)
            with open(os.path.join("src", fname), "w", newline="") as f:
                f.write(r.text)

            val += 1
            self.progress_changed.emit(val)

        return True, "Downloaded version " + vers_r.text + ". Restart program to update."

if __name__ == '__main__':
    # set exepthook to not save in case there's an error with loading the data
    sys.excepthook = exception_hook_no_save
    app = QtWidgets.QApplication(sys.argv)
    updater = Updater()
    updater.update()
    if updater.exec():
        sys.exit(0)

    data = Data()
    if os.path.exists(ECONOMY_FILE):
        data.read_from_file(ECONOMY_FILE)
    else:
        data.set_defaults()

    # now the data has been loaded successfully, set normal excepthook that saves in case of error
    sys.excepthook = exception_hook
    ex = Main(data)
    if os.path.isfile("stylesheets.qss"):
        with open("stylesheets.qss", "r") as f:
            ex.setStyleSheet(f.read())
    if os.path.isfile("appearance.json"):
        with open("appearance.json", "r") as f:
            ap = json.load(f)

        app.setStyle(ap["style"])

    sys.exit(app.exec_())


import numpy as np
from backend.NumericalTools import derive
import matplotlib.pyplot as plt
import json, os, sys
import pandas as pd
from scipy import interpolate
from typing import Union

datafolder_path = '\\'.join(__file__.split('/')[:-3]) if len(__file__.split('/')) > 1 else '\\'.join(__file__.split('\\')[:-3])
datafiles_path = datafolder_path + '\\data\\AirfoilCoordinates\\processed'

# datafiles_path = '/'.join(__file__.split('/')[:-3]) + 'data/AirfoilCoordinates/processed'
print(__file__, datafiles_path)
AIRFOILS = [file.split('.')[0] for file in os.listdir(datafiles_path)]

class AirFoil(object):

    def __init__(self, chord: int or float):

        self.chord = chord
        self.coordinates = None

    @staticmethod
    def __turning_point(arr):

        oldval = arr[0]
        for idx, value in enumerate(arr[1:]):
            if value > oldval:
                return idx

            else:
                oldval = float(value)

        raise ValueError("Could not find turning point in given arrays. "
                         "Make sure the coordinates are defined from upper trailing edge to lower trailing edge.")

    def __find_turning_point(self):

        x = self.coordinates['x']

        return self.__turning_point(x)

    def spline_coordinate_calculation(self, xnew: Union[list, np.array, str], k: int = 3, s: int = 0, **kwargs):

        arrsort = lambda xarr, zarr: np.array(
            [[xi, zi] for xi, zi in sorted(zip(xarr, zarr),
                                           key=lambda pair:
                                           pair[0])
             ]
        )

        if type(xnew) == list:
            xnew = np.array(xnew)

        elif type(xnew) == str:

            if 'n' not in kwargs.keys():
                kwargs['n'] = 50

            xnew = xnew.lower()
            if xnew == 'uniform' or xnew == 'linear':
                xnew = np.concatenate((np.linspace(1, 0, kwargs['n']), np.linspace(0, 1, kwargs['n'])))

            elif xnew == 'cosine_spacing' or xnew == 'cosinespacing' or xnew == 'cosine':
                cosine = 0.5 * (1 - np.cos(np.linspace(0, np.pi, kwargs['n']))) * self.chord
                xnew = np.concatenate((cosine[::-1], cosine))

        idx_t_old = self.__find_turning_point()
        idx_t_new = self.__turning_point(xnew)

        upper_coordinates = arrsort(self.coordinates['x'][:idx_t_old], self.coordinates['z'][:idx_t_old])
        lower_coordinates = arrsort(self.coordinates['x'][idx_t_old:], self.coordinates['z'][idx_t_old:])

        tU, cU, kU = interpolate.splrep(upper_coordinates[:, 0], upper_coordinates[:, 1], k=k, s=s)
        tL, cL, kL = interpolate.splrep(lower_coordinates[:, 0], lower_coordinates[:, 1], k=k, s=s)

        upper_spline = interpolate.BSpline(tU, cU, kU, extrapolate=False)
        lower_spline = interpolate.BSpline(tL, cL, kL, extrapolate=False)

        self.coordinates = {
            'x': xnew,
            'z': np.concatenate((upper_spline(xnew[:idx_t_new]), lower_spline(xnew[idx_t_new:])))
        }
        self.coordinates['z'][np.isnan(self.coordinates['z'])] = 0

        return self.coordinates

class NACAFoil(AirFoil):

    def __init__(self, code: str or int, n_digits: int, chord: int or float, **kwargs):

        super().__init__(chord=chord)

        self.__n = n_digits

        code = str(code)
        if len(code) != self.__n:
            raise ValueError(f"Expected a {self.__n}-Digit Input, got a {len(code)}-Digit input")
        else:
            for char in code:
                if ord(char) < 48 or ord(char) > 57:
                    raise TypeError("Expected a numerical code, got characters instead")

        self.code = code

        # Plotting Parameters
        self.__m = None
        self.__p = None
        self.__t = None
        self.__k1 = None
        self.__initialize_parameters()

        # Plotting Functions
        self.__yt = self.__thickness_distribution_functions(self.__t, self.chord)
        self.__yc_p = lambda: None
        self.__yc_c = lambda: None
        self.__theta_p = lambda: None
        self.__theta_c = lambda: None

    def __str__(self):
        """
        Method to support printing the object in console
        """
        return f"NACA-{self.code}"

    def __mul__(self, other: int or float):
        """
        This will allow multiplication with the object.
        This will multiply the chord with the other and adjust the thickness accordingly
        :param other: Factor with which to multiply the object. Can only be positive non-zero numbers.
        """

        if type(other) not in [int, float]:
            raise TypeError(f"Cannot multiply with type {type(other)}, expected int or float")

        self.chord *= other if other >= 0 else 1

    def __add__(self, other: int or float):
        """
        This will allow addition to be perfroemd on the object.
        The other number will be added to the chord and will adjust the thickness accordingly
        :param other: Number to add to chord. Cannot be smaller than current chord
        """

        if type(other) not in [int, float]:
            raise TypeError(f"Cannot add with type {type(other)}, expected int or float")

        self.chord += other if other >= -self.chord else 0

    def __initialize_parameters(self):
        """
        Will initialize the parameters used to describe the geometry for the airfoils.
        """

        if self.__n == 4:
            self.__m = int(self.code[0]) / 100   # Maximum camber
            self.__p = int(self.code[1]) / 10    # Position of maximum camber
            self.__t = int(self.code[2:]) / 100  # Maximum thickness

        elif self.__n == 5:

            if sys.platform == 'win32':
                datafile = '\\'.join(datafiles_path.split('\\')[:-3])+'\\data\\fivedigit_coefficients.json'

            else:
                datafile = '/'.join(datafiles_path.split('/')[:-3]) + 'data/fivedigit_coefficients.json'

            with open(datafile, 'r') as file:
                coefficients = json.load(file)[self.code[0:3]]

            self.__m = coefficients['m']         # Camber function parameter
            self.__p = coefficients['p']         # Position of Maximum Camber
            self.__k1 = coefficients['k1']       # Camber function parameter
            self.__t = int(self.code[3:]) / 100  # Maximum thickness

    @staticmethod
    def __thickness_distribution_functions(t, c):
        """
        Method to calculate the thickness distribution for the airfoil. Is identical for 4- and 5-digit airfoils.
        :param t: thickness value
        :param c: chordlength
        :return: function with as input a number from 0 to c and returning airfoil thickness.
        """

        yt = lambda x: t / 0.2 * (0.2969 * np.sqrt(x / c) -
                                         0.1260 * (x / c) -
                                         0.3515 * (x / c) ** 2 +
                                         0.2843 * (x / c) ** 3 -
                                         0.1015 * (x / c) ** 4) * c

        return yt

    @staticmethod
    def __modified_thickness_distribution():
        pass

    # TODO: Implement modified thickness functions
    # TODO: Finish copying coefficients for modified 20% thickness airfoils

    @staticmethod
    def __theta_calculation(yc_p, yc_c):
        """
        Method to calculate the slope of the airfoil. Is identical for 4- and 5-digit airfoils.
        :param yc_p: Function describing the camberline with as input a number from 0 to p
        :param yc_c:
        :return:
        """

        dyc_dx_p = derive(yc_p)
        dyc_dx_c = derive(yc_c)

        theta_p = lambda x: np.arctan(dyc_dx_p(x))
        theta_c = lambda x: np.arctan(dyc_dx_c(x))

        return theta_p, theta_c

    def get_coordinates(self):
        return self.coordinates

    def load_coordinates(self, cosine_spacing: bool = False, n: int = 25):

        data = {
            'XU': [],
            'YU': [],
            'XL': [],
            'YL': []
        }

        if cosine_spacing is False:
            xrange = np.linspace(0, self.chord, n)

        elif cosine_spacing is True:
            xrange = 0.5*(1 - np.cos(np.linspace(0, np.pi, n)))*self.chord

        else:
            raise ValueError(f"Expected a Boolean, got {type(cosine_spacing)} instead")

        for x in xrange:

            if x <= self.__p * self.chord:
                data['XU'].append(x - self.__yt(x) * np.sin(self.__theta_p(x)))
                data['YU'].append(self.__yc_p(x) + self.__yt(x) * np.cos(self.__theta_p(x)))

                data['XL'].append(x + self.__yt(x) * np.sin(self.__theta_p(x)))
                data['YL'].append(self.__yc_p(x) - self.__yt(x) * np.cos(self.__theta_p(x)))

            elif self.chord >= x > self.chord * self.__p:
                data['XU'].append(x - self.__yt(x) * np.sin(self.__theta_c(x)))
                data['YU'].append(self.__yc_c(x) + self.__yt(x) * np.cos(self.__theta_c(x)))

                data['XL'].append(x + self.__yt(x) * np.sin(self.__theta_c(x)))
                data['YL'].append(self.__yc_c(x) - self.__yt(x) * np.cos(self.__theta_c(x)))

        x = np.concatenate((data['XU'][::-1], data['XL']))
        z = np.concatenate((data['YU'][::-1], data['YL']))

        self.coordinates = {"x": x, "z": z}

        return self.coordinates

class FourDigitNACA(NACAFoil):

    def __init__(self, code: str or int, chord: int or float, **kwargs):

        # Initialize Parent NACA Class
        super().__init__(code=code, chord=chord, n_digits=4, **kwargs)

        self.__p = self._NACAFoil__p
        self.__m = self._NACAFoil__m

        if self.__p == 0 and self.__m == 0:
            self.__type = 'symmetrical'

        else:
            self.__type = 'cambered'

        self.__yt = self._NACAFoil__yt
        self._NACAFoil__yc_p, self._NACAFoil__yc_c = self.__mean_camber_line()
        self._NACAFoil__theta_p, self._NACAFoil__theta_c = self._NACAFoil__theta_calculation(self._NACAFoil__yc_p, self._NACAFoil__yc_c)

    def __mean_camber_line(self):

        if self.__type == 'cambered':
            yc_0 = lambda x: self.__m / (self.__p ** 2) * (2 * self.__p * x / self.chord - (x / self.chord) ** 2) * self.chord
            yc_1 = lambda x: self.__m / ((1 - self.__p) ** 2) * (1 - 2 * self.__p + 2 * self.__p * x / self.chord - (x / self.chord) ** 2) * self.chord

        elif self.__type == 'symmetrical':
            yc_0 = lambda x: 0
            yc_1 = lambda x: 0

        else:
            raise TypeError("Airfoil type not defined")

        return yc_0, yc_1

class FiveDigitNACA(NACAFoil):

    def __init__(self, code: str or int, chord: int or float, **kwargs):

        # Initialize Parent NACA Class
        super().__init__(code=code, chord=chord, n_digits=5, **kwargs)

        self.__p = self._NACAFoil__p
        self.__m = self._NACAFoil__m
        self.__k1 = self._NACAFoil__k1

        self.__yt = self._NACAFoil__yt
        self._NACAFoil__yc_p, self._NACAFoil__yc_c = self.__mean_camber_line()
        self._NACAFoil__theta_p, self._NACAFoil__theta_c = self._NACAFoil__theta_calculation(self._NACAFoil__yc_p,
                                                                                self._NACAFoil__yc_c)

    def __mean_camber_line(self):

        yc_0 = lambda x: self.__k1/6*(x**3 - 3*self.__m*x**2+self.__m**2*(3-self.__m)*x)
        yc_1 = lambda x: self.__k1/6*self.__m**3*(1-x)

        return yc_0, yc_1

class LoadedAirfoil(AirFoil):

    def __init__(self, code: str, chord: int or float = 1.0):

        super().__init__(chord=chord)

        self.code = code.lower()

    def load_coordinates(self, cosine_spacing: bool = False, n: int = 25):

        self.coordinates = self.__load_airfoil()

        if cosine_spacing is False:
            xrange = 'linear'

        elif cosine_spacing is True:
            xrange = 'cosine'

        # self.coordinates = self.spline_coordinate_calculation(xrange)

        return self.spline_coordinate_calculation(xrange, n=n)

    def __load_airfoil(self):

        if sys.platform == 'win32':
            datafiles_path = '\\'.join(os.getcwd().split('\\')[:-2]) + '\\data\\AirfoilCoordinates\\processed'

        else:
            datafiles_path = '/'.join(os.getcwd().split('/')[:-2]) + 'data/AirfoilCoordinates/processed'

        airfoils = AIRFOILS

        if self.code not in airfoils:
            raise ValueError("Specified Airfoil not found in database")

        else:
            coordinates = pd.read_csv(datafiles_path+f"\\{self.code}.txt",
                                      sep=',',
                                      index_col=False,
                                      skiprows=[0],
                                      header=None,
                                      names=['x', 'z'],
                                      dtype=float)

        data = {"x": np.array(coordinates['x'], dtype=float),
                "z": np.array(coordinates['z'], dtype=float)}

        return data

if __name__ == '__main__':

    test4d = FourDigitNACA('3210', chord=4)
    a = test4d.load_coordinates(cosine_spacing=False)

    test5d = FiveDigitNACA(23012, chord=1)
    b = test5d.load_coordinates(cosine_spacing=True)

    testL = LoadedAirfoil('e1213')
    c = testL.load_coordinates(cosine_spacing=True)

    fig = plt.figure()
    plt.plot(a['x'], a['z'], 'ko')
    plt.axis('equal')  # to preserve the aspect ratio of the plot
    plt.grid()
    plt.show()

    fig = plt.figure()
    plt.plot(b['x'], b['z'], 'ko')
    plt.axis('equal')  # to preserve the aspect ratio of the plot
    plt.grid()
    plt.show()

    fig = plt.figure()
    plt.plot(c['x'], c['z'], 'ko')
    plt.axis('equal')  # to preserve the aspect ratio of the plot
    plt.grid()
    plt.show()