#!/usr/bin/env python
import csv
import os
#import io
import copy
import pyqrcode
from datetime import datetime #, timedelta
import matplotlib.pyplot as plt
import pandas as pd
from fpdf import FPDF

class PDF(FPDF):
    def header(self):
        # logo
        #self.image('fox_face.png', 10, 8, 25)
        # font
        self.set_font('times', 'B', 20)
        # Title
        self.cell(0, 0, 'Bitpanda Report', ln=True, align = 'C')
        # line break
        self.ln(10)

    def footer(self):
        # set position of the footer
        self.set_y(-15)
        # set font
        self.set_font('times', 'I', 10)
        # Date
        self.cell(0, 10, f'{today.strftime("%d/%m/%Y")}', align = 'L')
        # Page number
        self.cell(0, 10, f'{self.page_no()}/nb', align = 'R')

def removeFiles(gotNames):
    for rows in gotNames:
        if os.path.exists(f"{rows}.png"):
            os.remove(f"{rows}.png")
    if os.path.exists('newTempFile0x123.csv'):
        os.remove('newTempFile0x123.csv')

def generatePieChart(assets_Account, titleName):
    # Pie Chart
    x_list = []
    label_list = []
    for row in range(len(assets_Account)):
        x_list.append(assets_Account[row]["Amount"])
        label_list.append(f'{assets_Account[row]["Asset"]}\n({assets_Account[row]["Amount"]})')
    plt.axis("equal") # Kreisdiagramm rund gestaltet (sonst Standard: oval!)
    plt.pie(x_list, labels=label_list, autopct="%1.1f%%")
    plt.title(titleName)
    plt.savefig(f'{titleName}.png', bbox_inches='tight')
    #print(f'{titleName} generated')
    plt.close()

    return titleName

csv_path_to_file = ''
csv_filename = ''
if csv_path_to_file == '':
    path_input = input("Pfad zur csv-Datei (Drag&Drop, remove ''): ")
    csv_Oldpath = os.path.join(path_input)
else:
    csv_Oldpath = os.path.join(csv_path_to_file, csv_filename)

userKey = 'USER'
os_Name = os.name
if os_Name == 'posix':
    userKey = 'USER'
    csvDf = pd.read_csv(csv_Oldpath, skiprows=6)
elif os_Name == 'nt':
    userKey = 'USERNAME'
    csvDf = pd.read_csv(csv_Oldpath, skiprows=6)
else:
    csvDf = pd.read_csv(csv_Oldpath, skiprows=6)
    userKey = 'USERNAME'
csvDf.to_csv('newTempFile0x123.csv', index=False)
newPath_input = 'newTempFile0x123.csv'
csv_path = os.path.join(newPath_input)
input_file = csv.DictReader(open(csv_path))

gitlink = 'https://github.com/MrRo-de/Bitpanda-Report'

disclaimer_eng = '- Disclaimer: All data is without guarantee, errors and changes are reserved. -'
disclaimer_deu = '- Haftungsausschluss: Alle Angaben ohne Gewähr, Irrtümer und Änderungen vorbehalten. -'

today = datetime.today()
print(f"heute ist der {today}\n")

full_name = input('Dein Namen:\n')
street = input('Straße, Hausnummer:\n')
postcode_city = input('PLZ und Ort:\n')
"""
full_name = ''
street = ''
postcode_city = ''
"""
input_file = csv.DictReader(open(csv_path))
fiat_assets = []
metal_assets = []
crypto_assets = []
stock_assets = []

################################################################################################
#################### Find all Assets [Fiat, Metal, Cryptocurrency, Stocks] #####################
################################################################################################

for row in input_file:
    if row['Asset class'] == 'Fiat':
        fiat_assets.append(row["Asset"])
    elif row['Asset class'] == 'Metal':
        metal_assets.append(row["Asset"])
    elif row['Asset class'] == 'Cryptocurrency':
        crypto_assets.append(row["Asset"])
    elif row['Asset class'] == 'Stock (derivative)':
        stock_assets.append(row["Asset"])

fiat_assets = set(fiat_assets)
metal_assets = set(metal_assets)
crypto_assets = set(crypto_assets)
stock_assets = set(stock_assets)

fiat_list = []
#print(fiat_assets)
fiat_paid = 0.00

for asset in fiat_assets:

    input_file = csv.DictReader(open(csv_path))
    fiat_amount = 0.00

    for row in input_file :
        if asset == row["Asset"]:
            if row["Amount Fiat"] == '-':
                row["Amount Fiat"] = '0.00'
            if row['Fee'] == '-':
                row['Fee'] = '0.00'

            if (row['Transaction Type'] == 'buy'):
                fiat_amount += float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'sell'):
                fiat_amount -= float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'deposit'):
                fiat_amount += float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'withdrawal'):
                fiat_amount -= float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'transfer'):
                if (row['In/Out'] == 'outgoing'):
                    fiat_amount -= float(row["Amount Fiat"])
                elif (row['In/Out'] == 'incoming'):
                    fiat_amount += float(row["Amount Fiat"])

            if (float(row['Fee']) >= 0.00):
                fiat_amount -= float(row['Fee'])

    fiat_amount = '{:.2f}'.format(fiat_amount)
    if float(fiat_amount) > 0.0:
        fiat_list.append({"Asset": asset, "Amount": fiat_amount})

#print(fiat_list)

metal_list = []
#print(metal_assets)
for asset in metal_assets:

    input_file = csv.DictReader(open(csv_path))
    metal_amount = 0.00

    for row in input_file :
        if asset == row["Asset"]:
            if row["Amount Asset"] == '-':
                row["Amount Asset"] = '0.00'
            if row['Fee'] == '-':
                row['Fee'] = '0.00'

            if (row['Transaction Type'] == 'buy'):
                metal_amount += float(row["Amount Asset"])
                fiat_paid -= float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'sell'):
                metal_amount -= float(row["Amount Asset"])
                fiat_paid += float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'deposit'):
                metal_amount += float(row["Amount Asset"])
            elif (row['Transaction Type'] == 'withdrawal'):
                metal_amount -= float(row["Amount Asset"])
            elif (row['Transaction Type'] == 'transfer'):
                metal_amount += float(row["Amount Asset"])

            if (float(row['Fee']) >= 0.00):
                metal_amount -= float(row['Fee'])

    metal_amount = '{:.6f}'.format(metal_amount)
    if float(metal_amount) > 0.000000:
        metal_list.append({"Asset": asset, "Amount": metal_amount})

#print(metal_list)

crypto_list = []
#print(crypto_assets)
for asset in crypto_assets:

    input_file = csv.DictReader(open(csv_path))
    crypto_amount = 0.00

    for row in input_file :
        if asset == row["Asset"]:
            if row["Amount Asset"] == '-':
                row["Amount Asset"] = '0.00'
            if row['Fee'] == '-':
                row['Fee'] = '0.00'

            if (row['Transaction Type'] == 'buy'):
                crypto_amount += float(row["Amount Asset"])
                fiat_paid -= float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'sell'):
                crypto_amount -= float(row["Amount Asset"])
                fiat_paid += float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'deposit'):
                crypto_amount += float(row["Amount Asset"])
            elif (row['Transaction Type'] == 'withdrawal'):
                crypto_amount -= float(row["Amount Asset"])
            elif (row['Transaction Type'] == 'transfer'):
                crypto_amount += float(row["Amount Asset"])

            if (float(row['Fee']) >= 0.00):
                crypto_amount -= float(row['Fee'])

    crypto_amount = '{:.6f}'.format(crypto_amount)
    if float(crypto_amount) > 0.000000:
        crypto_list.append({"Asset": asset, "Amount": crypto_amount})

#print(crypto_list)

stock_list = []
for asset in stock_assets:

    input_file = csv.DictReader(open(csv_path))
    stock_amount = 0.00

    for row in input_file :
        if asset == row["Asset"]:
            if row["Amount Asset"] == '-':
                row["Amount Asset"] = '0.00'
            if row['Fee'] == '-':
                row['Fee'] = '0.00'

            if (row['Transaction Type'] == 'buy'):
                stock_amount += float(row["Amount Asset"])
                fiat_paid -= float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'sell'):
                stock_amount -= float(row["Amount Asset"])
                fiat_paid += float(row["Amount Fiat"])
            elif (row['Transaction Type'] == 'deposit'):
                stock_amount += float(row["Amount Asset"])
            elif (row['Transaction Type'] == 'withdrawal'):
                stock_amount -= float(row["Amount Asset"])
            elif (row['Transaction Type'] == 'transfer'):
                stock_amount += float(row["Amount Asset"])

            if (float(row['Fee']) >= 0.00):
                stock_amount -= float(row['Fee'])

    stock_amount = '{:.6f}'.format(stock_amount)
    if float(crypto_amount) > 0.000000:
        stock_list.append({"Asset": asset, "Amount": stock_amount})

#print(stock_list)
#fiat_list = [{'Asset': 'USD', 'Amount': '985.00'}, {'Asset': 'EUR', 'Amount': '-1465.00'}, {'Asset': 'CHZ', 'Amount': '-0.00'}]
for row in fiat_list:
    if row["Asset"] == 'EUR':
        old = float(row["Amount"])
        calc = old + fiat_paid
        if calc == -0.00:
            calc = 0.00
        if calc <= 0.00:
            calc = 0.00

        row["Amount"] = '{:.2f}'.format(calc)

#print(fiat_list)

################################################################################################
################################ Generate Pie Charts ###########################################
################################################################################################

numberOfCharts = 0
listOfPNG = []

temp_fiat_list = copy.deepcopy(fiat_list)
fiat_list.clear()
for row in temp_fiat_list:
    if abs(float(row['Amount'])) != 0.000000:
        fiat_list.append(row)
#print(fiat_list)
if len(fiat_list) > 0:
    listOfPNG.append(generatePieChart(fiat_list, 'Dein Fiat Portfolio'))
    numberOfCharts += 1

temp_metal_list = copy.deepcopy(metal_list)
metal_list.clear()
for row in temp_metal_list:
    if abs(float(row['Amount'])) != 0.000000:
        metal_list.append(row)
#print(metal_list)
if len(metal_list) > 0:
    listOfPNG.append(generatePieChart(metal_list, 'Dein Metal Portfolio'))
    numberOfCharts += 1

temp_crypto_list = copy.deepcopy(crypto_list)
crypto_list.clear()
for row in temp_crypto_list:
    if abs(float(row['Amount'])) != 0.000000:
        crypto_list.append(row)
#print(crypto_list)
if len(crypto_list) > 0:
    listOfPNG.append(generatePieChart(crypto_list, 'Dein Crypto Portfolio'))
    numberOfCharts += 1

temp_stock_list = copy.deepcopy(stock_list)
stock_list.clear()
for row in temp_stock_list:
    if abs(float(row['Amount'])) != 0.000000:
        stock_list.append(row)
#print(stock_list)
if len(stock_list) > 0:
    listOfPNG.append(generatePieChart(stock_list, 'Dein Stock Portfolio'))
    numberOfCharts += 1

################################################################################################
################################# Get all Transactions #########################################
################################################################################################

input_file = csv.DictReader(open(csv_path))
csvDate_format = '%Y-%m-%d'
csvTime_format = '%H:%M:%S'

fiat_in_dict = []
metal_in_dict = []
crypto_in_dict = []
stock_in_dict = []
fiat_out_dict = []
metal_out_dict = []
crypto_out_dict = []
stock_out_dict = []

temp_fiat_dict = []
temp_metal_dict = []
temp_crypto_dict = []
temp_stock_dict = []

for row in input_file:
    date = datetime.strptime(row['Timestamp'][:10], csvDate_format)
    time = datetime.strptime(row['Timestamp'][11:19], csvTime_format)
    if row['Asset class'] == 'Fiat':
        if (row['Transaction Type'] == 'buy'):
            fiat_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Kauf", "Betrag": row["Amount Fiat"], "Asset": row["Fiat"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'sell'):
            fiat_out_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Verkauf", "Betrag": row["Amount Fiat"], "Asset": row["Fiat"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'deposit'):
            fiat_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "einzahlen", "Betrag": row["Amount Fiat"], "Asset": row["Fiat"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'withdrawal'):
            fiat_out_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "auszahlen", "Betrag": row["Amount Fiat"], "Asset": row["Fiat"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'transfer'):
            if (row['In/Out'] == 'outgoing'):
                fiat_out_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "versenden", "Betrag": row["Amount Fiat"], "Asset": row["Fiat"], "Gebühren": row["Fee"]})
            elif (row['In/Out'] == 'incoming'):
                fiat_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "empfangen", "Betrag": row["Amount Fiat"], "Asset": row["Fiat"], "Gebühren": row["Fee"]})
    elif row['Asset class'] == 'Metal':
        if (row['Transaction Type'] == 'buy'):
            metal_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Kauf", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'sell'):
            metal_out_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Verkauf", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'deposit'):
            metal_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "einzahlen", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'withdrawal'):
            metal_out_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "auszahlen", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'transfer'):
            metal_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "erhalten", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
    elif row['Asset class'] == 'Cryptocurrency':
        if (row['Transaction Type'] == 'buy'):
            crypto_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Kauf", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'sell'):
            crypto_out_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Verkauf", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'deposit'):
            crypto_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "empfangen", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'withdrawal'):
            crypto_out_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "versenden", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'transfer'):
            if row['Asset'] == 'BEST':
                crypto_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Rewards", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
            else:
                crypto_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "erhalten", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
    elif row['Asset class'] == 'Stock (derivative)':
        if (row['Transaction Type'] == 'buy'):
            stock_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Kauf", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'sell'):
            stock_out_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Verkauf", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'deposit'):
            stock_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "empfangen", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'withdrawal'):
            stock_out_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "versenden", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
        elif (row['Transaction Type'] == 'transfer'):
            if row['Asset'] == 'BEST':  # Ist integriert für den Fall von Dividenden / Muss jedoch angepasst werden.
                stock_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "Rewards", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})
            else:
                stock_in_dict.append({"Datum": f'{date.date()} {time.time()}', "Transaktion": "erhalten", "Betrag": row["Amount Fiat"], "Asset Menge": row["Amount Asset"], "Asset Preis": row["Asset market price"], "Asset": row["Asset"], "Gebühren": row["Fee"]})

fiat_in_dict = sorted(fiat_in_dict, key=lambda k: k['Datum'])
fiat_out_dict = sorted(fiat_out_dict, key=lambda k: k['Datum'])
metal_in_dict = sorted(metal_in_dict, key=lambda k: k['Datum'])
metal_out_dict = sorted(metal_out_dict, key=lambda k: k['Datum'])
crypto_in_dict = sorted(crypto_in_dict, key=lambda k: k['Datum'])
crypto_out_dict = sorted(crypto_out_dict, key=lambda k: k['Datum'])
stock_in_dict = sorted(stock_in_dict, key=lambda k: k['Datum'])
stock_out_dict = sorted(stock_out_dict, key=lambda k: k['Datum'])

temp_fiat_dict = fiat_in_dict + fiat_out_dict
temp_metal_dict = metal_in_dict + metal_out_dict
temp_crypto_dict = crypto_in_dict + crypto_out_dict
temp_stock_dict = stock_in_dict + stock_out_dict

temp_fiat_dict = sorted(temp_fiat_dict, key=lambda k: k['Datum'])
temp_metal_dict = sorted(temp_metal_dict, key=lambda k: k['Datum'])
temp_crypto_dict = sorted(temp_crypto_dict, key=lambda k: k['Datum'])
temp_stock_dict = sorted(temp_stock_dict, key=lambda k: k['Datum'])

################################################################################################
############################### Calculate Win and Losses #######################################
################################################################################################

metal_winloss = []
temp_metal_steuern = []
metal_steuern = []
metal_portfolio = []
crypto_winloss = []
temp_crypto_steuern = []
crypto_steuern = []
crypto_portfolio = []
stock_winloss = []
temp_stock_steuern = []
stock_steuern = []
stock_portfolio = []
steuern = []
steuernCalc = []

#{"Asset": asset, "HODL": days.days, "Jahr": year.year, "winLoss": '{:.2f}'.format(temp_win_loss)}
def calcMetalSteuern(asset, winloss):
    temp_year = datetime.strptime('1990-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
    temp_toPay = 0.00
    temp_Asset = ''
    for row in winloss:
        if row['Asset'][-1] == '*':
            temp_Asset = row['Asset'][:-1]
        else:
            temp_Asset = row['Asset']
        if asset == temp_Asset:
            if int(row['HODL']) <= 365:
                if row['Jahr'] == temp_year:
                    temp_toPay += float(row['winLoss'])
                else:
                    if temp_toPay != 0.00:
                        steuernCalc.append({"Asset": row['Asset'], "verkaufs Jahr": temp_year, "Betrag": temp_toPay})
                    temp_toPay = float(row['winLoss'])
                    temp_year = row['Jahr']

    if temp_toPay != 0.00:
        steuernCalc.append({"Asset": row['Asset'], "verkaufs Jahr": temp_year, "Betrag": temp_toPay})
    return steuernCalc

def calcCryptoSteuern(asset, winloss):
    temp_year = datetime.strptime('1990-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
    temp_toPay = 0.00
    temp_Asset = ''
    for row in winloss:
        if row['Asset'][-1] == '*':
            temp_Asset = row['Asset'][:-1]
        else:
            temp_Asset = row['Asset']
        if asset == temp_Asset:
            if int(row['HODL']) <= 365:
                if row['Jahr'] == temp_year:
                    temp_toPay += float(row['winLoss'])
                else:
                    if temp_toPay != 0.00:
                        steuernCalc.append({"Asset": row['Asset'], "verkaufs Jahr": temp_year, "Betrag": temp_toPay})
                    temp_toPay = float(row['winLoss'])
                    temp_year = row['Jahr']

    if temp_toPay != 0.00:
        steuernCalc.append({"Asset": row['Asset'], "verkaufs Jahr": temp_year, "Betrag": temp_toPay})
    return steuernCalc

def calcStockSteuern(asset, winloss):
    temp_year = datetime.strptime('1990-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
    temp_toPay = 0.00
    temp_Asset = ''
    for row in winloss:
        if row['Asset'][-1] == '*':
            temp_Asset = row['Asset'][:-1]
        else:
            temp_Asset = row['Asset']
        if asset == temp_Asset:
            if row['Jahr'] == temp_year:
                temp_toPay += float(row['winLoss'])
            else:
                if temp_toPay != 0.00:
                    steuernCalc.append({"Asset": row['Asset'], "verkaufs Jahr": temp_year, "Betrag": temp_toPay})
                temp_toPay = float(row['winLoss'])
                temp_year = row['Jahr']

    if temp_toPay != 0.00:
        steuernCalc.append({"Asset": row['Asset'], "verkaufs Jahr": temp_year, "Betrag": temp_toPay})
    return steuernCalc

def calcWinLoss(in_dict, assets, out_dict, asset_class):

    test = ''
    winloss = []
    steuern_winloss = []
    steuern_metal = []
    steuern_crypto = []
    steuern_stock = []
    temp_winloss = []
    portfolio_dict = []
    for asset in assets:
        hint = False
        temp_in_dict = []
        for row_in in in_dict:
            if row_in['Asset'] == asset:
                if row_in['Betrag'] == '-':
                    row_in['Betrag'] = 0.00
                if row_in['Gebühren'] == '-':
                    row_in['Gebühren'] = 0.00
                if row_in["Transaktion"] == 'empfangen':
                    hint = True
                temp_in_dict.append({"Datum": row_in['Datum'], "Transaktion": row_in['Transaktion'], "Betrag": '{:.2f}'.format(float(row_in["Betrag"])), "Asset Menge": '{:.6f}'.format(float(row_in["Asset Menge"])), "Asset Preis": '{:.2f}'.format(float(row_in["Asset Preis"])), "Asset": row_in["Asset"], "Gebühren": '{:.6f}'.format(float(row_in["Gebühren"]))})

        temp_in_dict = sorted(temp_in_dict, key=lambda k: k['Datum'])

        temp_out_dict = []
        for row_out in out_dict:
            if row_out['Asset'] == asset:
                if row_out['Betrag'] == '-':
                    row_out['Betrag'] = 0.00
                if row_out['Gebühren'] == '-':
                    row_out['Gebühren'] = 0.00
                temp_out_dict.append({"Datum": row_out['Datum'], "Transaktion": row_out['Transaktion'], "Betrag": '{:.2f}'.format(float(row_out["Betrag"])), "Asset Menge": '{:.6f}'.format(float(row_out["Asset Menge"])), "Asset Preis": '{:.2f}'.format(float(row_out["Asset Preis"])), "Asset": row_out["Asset"], "Gebühren": '{:.6f}'.format(float(row_out["Gebühren"]))})

        temp_out_dict = sorted(temp_out_dict, key=lambda k: k['Datum'])

        win_loss = 0.00
        temp_win_loss = 0.00
        temp2_win_loss = 0.00
        #index = 0
        for row_out in temp_out_dict:
            temp_win_loss = 0.00
            if row_out['Asset'] == asset:
                days = datetime.strptime(row_out["Datum"], '%Y-%m-%d %H:%M:%S')-datetime.strptime(temp_in_dict[0]["Datum"], '%Y-%m-%d %H:%M:%S')
                year = datetime.strptime(row_out['Datum'], '%Y-%m-%d %H:%M:%S')
                if float(row_out['Asset Menge']) == 0.000000:
                    row_out['Asset Menge'] = row_out['Gebühren']

                if len(temp_in_dict) > 0:
                    if float(row_out['Asset Menge']) > float(temp_in_dict[0]['Asset Menge']):
                        temp2_win_loss = 0.00
                        while float(row_out['Asset Menge']) > float(temp_in_dict[0]['Asset Menge']):
                            #print(f'1: {len(temp_in_dict)}')
                            if row_out['Transaktion'] == 'Verkauf':
                                temp2_win_loss += (float(temp_in_dict[0]['Asset Menge']) * float(row_out['Asset Preis'])) - float(temp_in_dict[0]['Betrag'])
                            row_out['Betrag'] = '{:.2f}'.format((float(row_out['Asset Menge']) - float(temp_in_dict[0]['Asset Menge'])) * float(row_out['Asset Preis']))
                            row_out['Asset Menge'] = '{:.6f}'.format(float(row_out['Asset Menge']) - float(temp_in_dict[0]['Asset Menge']))

                            if len(temp_in_dict) > 0:
                                del temp_in_dict[0]
                            else:
                                temp_in_dict[0]['Betrag'] = 0.00
                                temp_in_dict[0]['Asset Menge'] = 0.000000

                            if float(row_out['Asset Menge']) < 0.00000:
                                #print('break because of Amount')
                                break
                            #print(f'2: {len(temp_in_dict)}')
                            if len(temp_in_dict) == 0:
                                #print('break because of length')
                                break

                        temp_win_loss += temp2_win_loss

                    if len(temp_in_dict) > 0:
                        if float(row_out['Asset Menge']) == float(temp_in_dict[0]['Asset Menge']):
                            if row_out['Transaktion'] == 'Verkauf':
                                temp_win_loss = temp_win_loss + (float(row_out['Betrag']) - float(temp_in_dict[0]['Betrag']))
                            if len(temp_in_dict) > 0:
                                del temp_in_dict[0]
                            else:
                                temp_in_dict[0]['Betrag'] = 0.00
                                temp_in_dict[0]['Asset Menge'] = 0.000000
                        elif float(row_out['Asset Menge']) < float(temp_in_dict[0]['Asset Menge']):
                            temp_in_dict[0]['Betrag'] = '{:.2f}'.format((float(temp_in_dict[0]['Asset Menge']) - float(row_out['Asset Menge'])) * float(temp_in_dict[0]['Asset Preis']))
                            temp_in_dict[0]['Asset Menge'] = '{:.6f}'.format((float(temp_in_dict[0]['Asset Menge']) - float(row_out['Asset Menge'])))
                            if row_out['Transaktion'] == 'Verkauf':
                                temp_win_loss = temp_win_loss + (float(row_out['Betrag']) - (float(temp_in_dict[0]['Asset Preis']) * float(row_out['Asset Menge'])))

            win_loss += temp_win_loss
            if hint == True:
                steuern_winloss.append({"Asset": f'{asset}*', "HODL": days.days, "Jahr": year.year, "winLoss": '{:.2f}'.format(temp_win_loss)})
                #hint = False
            else:
                steuern_winloss.append({"Asset": asset, "HODL": days.days, "Jahr": year.year, "winLoss": '{:.2f}'.format(temp_win_loss)})

            if asset == test:
                print(win_loss)
                print(temp_win_loss)
        temp_win_loss = 0.00
        temp_winloss.clear()

        if len(temp_in_dict) > 0:
            for row in temp_in_dict:
                if float(row['Asset Menge']) > 0.0:
                    portfolio_dict.append(row)

        winloss.append({"Asset": asset, "winLoss": '{:.2f}'.format(win_loss)})

        if win_loss != 0.00:
            if asset_class == 'metal':
                steuern_metal = calcMetalSteuern(asset, steuern_winloss)
            elif asset_class == 'crypto':
                steuern_crypto = calcCryptoSteuern(asset, steuern_winloss)
            elif asset_class == 'stock':
                steuern_stock = calcStockSteuern(asset, steuern_winloss)
        else:
            if asset_class == 'metal':
                steuern_metal.append({"Asset": asset, "verkaufs Jahr": 1990, "Betrag": 0.00})
            elif asset_class == 'crypto':
                steuern_crypto.append({"Asset": asset, "verkaufs Jahr": 1990, "Betrag": 0.00})
            elif asset_class == 'stock':
                steuern_stock.append({"Asset": asset, "verkaufs Jahr": 1990, "Betrag": 0.00})

    if asset_class == 'metal':
        return winloss, steuern_metal, portfolio_dict
    if asset_class == 'crypto':
        return winloss, steuern_crypto, portfolio_dict
    if asset_class == 'stock':
        return winloss, steuern_stock, portfolio_dict

################################################################################################
################################## Generate PDF File ###########################################
################################################################################################

# create FPDF object
# Layout ('P', 'L')
# Unit ('mm', 'cm', 'in')
# Format ('A3', 'A4' (default), 'A5', 'Letter', 'Legal', (100,150))
pdf = PDF('P', 'mm')
pdf_width = 210
pdf_height = 297
# get total page numbers
pdf.alias_nb_pages(alias='nb')

# Add a Page
pdf.add_page()

# specify font
# fonts ('times', 'courier', 'helvetica', 'symbol', 'zpfdingbats')
# 'B' (bold), 'U' (underline), 'I' (italic), '' (regular), combination (i.e. ('BU'))
pdf.set_font('times', '', 14)

# Set auto page break
pdf.set_auto_page_break(auto=True, margin = 15)

################################################################################################
##################################### First PDF Page ###########################################
################################################################################################

fiat_assets = sorted(fiat_assets, key=lambda k: k)
metal_assets = sorted(metal_assets, key=lambda k: k)
crypto_assets = sorted(crypto_assets, key=lambda k: k)
stock_assets = sorted(stock_assets, key=lambda k: k)
'''
if os.path.exists("Dein Fiat Portfolio.png"):
    print('Fiat PNG exists')
if os.path.exists("Dein Crypto Portfolio.png"):
    print('Crypto PNG exists')
if os.path.exists("Dein Metal Portfolio.png"):
    print('Metal PNG exists')
if os.path.exists("Dein Stock Portfolio.png"):
    print('Stock PNG exists')
'''
# Add text
# w = width
# h = height
# txt = your text
# ln (0 False; 1 True - move cursor down to next line)
# border (0 False; 1 True - add border around cell)
pdf.ln(5)
pdf.cell(40, 8, f"{full_name}", ln=True, border=False)
pdf.cell(40, 8, f"{street}", ln=True)
pdf.cell(40, 8, f"{postcode_city}", ln=True)
pdf.ln(6)
pdf.set_font('times', 'B', 10)
pdf.cell(40, 8, f"Du hast gehandelt mit:\n", ln=True)
pdf.ln(3)
pdf.set_font('times', 'B', 10)
pdf.cell(40, 8, "Fiat\n", ln=True)
pdf.set_font('times', '', 10)
pdf.cell(40, 8, f"{fiat_assets}\n", ln=True)
pdf.set_font('times', 'B', 10)
pdf.cell(40, 8, "Metal\n", ln=True)
pdf.set_font('times', '', 10)
pdf.cell(40, 8, f"{metal_assets}\n", ln=True)
pdf.set_font('times', 'B', 10)
pdf.cell(40, 8, "Crypto\n", ln=True)
pdf.set_font('times', '', 10)
pdf.cell(40, 8, f"{crypto_assets}\n", ln=True)
pdf.set_font('times', 'B', 10)
pdf.cell(40, 8, "Aktien\n", ln=True)
pdf.set_font('times', '', 10)
pdf.cell(40, 8, f"{stock_assets}\n", ln=True)
if numberOfCharts == 4:
    if os.path.exists("Dein Fiat Portfolio.png"):
        pdf.image('Dein Fiat Portfolio.png', pdf_width-85, 140, 70)
    if os.path.exists("Dein Metal Portfolio.png"):
        pdf.image('Dein Metal Portfolio.png', 15, 200, 70)
    if os.path.exists("Dein Crypto Portfolio.png"):
        pdf.image('Dein Crypto Portfolio.png', 15, 140, 70)
    if os.path.exists("Dein Stock Portfolio.png"):
        pdf.image('Dein Stock Portfolio.png', pdf_width-85, 200, 70)
elif numberOfCharts == 3:
    count = 0
    if os.path.exists("Dein Fiat Portfolio.png"):
        if count == 0:
            pdf.image('Dein Fiat Portfolio.png', 15, 140, 70)
        elif count ==1:
            pdf.image('Dein Fiat Portfolio.png', pdf_width-85, 140, 70)
        else:
            pdf.image('Dein Fiat Portfolio.png', (pdf_width/2)-35, 200, 70)
        count += 1
    if os.path.exists("Dein Metal Portfolio.png"):
        if count == 0:
            pdf.image('Dein Metal Portfolio.png', 15, 140, 70)
        elif count == 1:
            pdf.image('Dein Metal Portfolio.png', pdf_width-85, 140, 70)
        else:
            pdf.image('Dein Metal Portfolio.png', (pdf_width/2)-35, 200, 70)
        count += 1
    if os.path.exists("Dein Crypto Portfolio.png"):
        if count == 0:
            pdf.image('Dein Crypto Portfolio.png', 15, 140, 70)
        elif count == 1:
            pdf.image('Dein Crypto Portfolio.png', pdf_width-85, 140, 70)
        else:
            pdf.image('Dein Crypto Portfolio.png', (pdf_width/2)-35, 200, 70)
        count += 1
    if os.path.exists("Dein Stock Portfolio.png"):
        if count == 0:
            pdf.image('Dein Stock Portfolio.png', 15, 140, 70)
        elif count == 1:
            pdf.image('Dein Stock Portfolio.png', pdf_width-85, 140, 70)
        else:
            pdf.image('Dein Stock Portfolio.png', (pdf_width/2)-35, 200, 70)
        count += 1
elif numberOfCharts == 2:
    count = 0
    if os.path.exists("Dein Fiat Portfolio.png"):
        if count == 0:
            pdf.image('Dein Fiat Portfolio.png', 15, 140, 100)
        else:
            pdf.image('Dein Fiat Portfolio.png', pdf_width-115, 140, 100)
        count += 1
    if os.path.exists("Dein Metal Portfolio.png"):
        if count == 0:
            pdf.image('Dein Metal Portfolio.png', 15, 140, 100)
        else:
            pdf.image('Dein Metal Portfolio.png', pdf_width-115, 140, 100)
        count += 1
    if os.path.exists("Dein Crypto Portfolio.png"):
        if count == 0:
            pdf.image('Dein Crypto Portfolio.png', 15, 140, 100)
        else:
            pdf.image('Dein Crypto Portfolio.png', pdf_width-115, 140, 100)
        count += 1
    if os.path.exists("Dein Stock Portfolio.png"):
        if count == 0:
            pdf.image('Dein Stock Portfolio.png', 15, 140, 100)
        else:
            pdf.image('Dein Stock Portfolio.png', pdf_width-115, 140, 100)
        count += 1
elif numberOfCharts == 1:
    if os.path.exists("Dein Fiat Portfolio.png"):
        pdf.image('Dein Fiat Portfolio.png', (pdf_width/2)-75, 140, 150)
    if os.path.exists("Dein Metal Portfolio.png"):
        pdf.image('Dein Metal Portfolio.png', (pdf_width/2)-75, 140, 150)
    if os.path.exists("Dein Crypto Portfolio.png"):
        pdf.image('Dein Crypto Portfolio.png', (pdf_width/2)-75, 140, 150)
    if os.path.exists("Dein Stock Portfolio.png"):
        pdf.image('Dein Stock Portfolio.png', (pdf_width/2)-75, 140, 150)

pdf.ln(140)
pdf.set_font('times', 'I', 12)
pdf.cell(0, 0, disclaimer_deu, ln=True, align="C")
pdf.ln(5)
pdf.set_font('times', '', 10)
pdf.cell(0, 0, disclaimer_eng, ln=True, align="C")
pdf.ln(5)
pdf.set_font('times', '', 10)

################################################################################################
############################# Detailed Transaction Pages #######################################
################################################################################################

def generateFiatTransactionPages(temp_dict_sorted, asset_set, asset_class):

    asset_set = sorted(asset_set, key=lambda k: k)

    for asset in asset_set:

        pdf.add_page()
        pdf.set_font('times', 'B', 10)
        pdf.cell(40, 8, f"Details der {asset} {asset_class} Transaktionen:\n", ln=True)
        pdf.set_font('times', '', 10)
        pdf.ln(5)
        col_width = (pdf_width-30)/4
        pdf.set_font('times', 'B', 10)
        th = pdf.font_size+2
        pdf.cell(col_width, th, "Datum", align='C', border=1)
        pdf.cell(col_width, th, "Transaktion", align='C', border=1)
        pdf.cell(col_width, th, "Betrag", align='C', border=1)
        pdf.cell(col_width, th, "Gebühren", align='C', border=1)
        pdf.ln(th)
        pdf.set_font('times', '', 9)
        th = pdf.font_size+2
        for row in temp_dict_sorted:
            if row['Asset'] == asset:
                pdf.cell(col_width, th, str(row["Datum"]), border=1)
                pdf.cell(col_width, th, str(row["Transaktion"]), border=1)
                pdf.cell(col_width, th, str(row["Betrag"]), border=1)
                pdf.cell(col_width, th, str(row["Gebühren"]), border=1)
                pdf.ln(th)

def generateTransactionPages(temp_dict_sorted, asset_set, winloss, asset_class):

    for asset in asset_set:
        hint = False
        pdf.add_page()
        pdf.set_font('times', 'B', 10)
        pdf.cell(40, 8, f"Details der {asset} {asset_class} Transaktionen:\n", ln=True)
        pdf.set_font('times', '', 10)
        pdf.ln(5)
        col_width = (pdf_width-30)/6
        pdf.set_font('times', 'B', 10)
        th = pdf.font_size+2
        pdf.cell(col_width, th, "Datum", align='C', border=1)
        pdf.cell(col_width, th, "Transaktion", align='C', border=1)
        pdf.cell(col_width, th, "Betrag", align='C', border=1)
        pdf.cell(col_width, th, "Asset Menge", align='C', border=1)
        pdf.cell(col_width, th, "Asset Preis", align='C', border=1)
        pdf.cell(col_width, th, "Gebühren", align='C', border=1)
        pdf.ln(th)
        pdf.set_font('times', '', 9)
        th = pdf.font_size+2
        for row in temp_dict_sorted:
            if row['Asset'] == asset:
                if row["Transaktion"] == 'empfangen':
                    hint = True
                pdf.cell(col_width, th, str(row["Datum"]), border=1)
                pdf.cell(col_width, th, str(row["Transaktion"]), border=1)
                pdf.cell(col_width, th, str(row["Betrag"]), border=1)
                pdf.cell(col_width, th, str(row["Asset Menge"]), border=1)
                pdf.cell(col_width, th, str(row["Asset Preis"]), border=1)
                pdf.cell(col_width, th, str(row["Gebühren"]), border=1)
                pdf.ln(th)

        for rows in winloss:
            if rows['Asset'] == asset:
                pdf.ln(th*2)
                pdf.set_font('times', 'B', 10)
                pdf.cell(45, th, f"Gewinn/Verlust: ")
                if float(rows['winLoss']) < 0.0:
                    pdf.set_text_color(225, 0, 0)
                elif float(rows['winLoss']) > 0.0:
                    pdf.set_text_color(0, 225, 0)
                else:
                    pdf.set_text_color(0, 0, 0)
                pdf.cell(100, th,f" {'{:.2f}'.format(float(rows['winLoss']))} EUR", ln=True)
                pdf.set_font('times', '', 10)
                pdf.set_text_color(0, 0, 0)

        if hint == True:
            pdf.set_text_color(225, 0, 0)
            pdf.ln(th)
            pdf.cell(45, th, f"* Durch das Einzahlen des Assets ist eine genaue Berechnung nicht möglich.")
            pdf.set_text_color(0, 0, 0)
            hint = False

temp_metal_in_dict = copy.deepcopy(metal_in_dict)
temp_metal_out_dict = copy.deepcopy(metal_out_dict)
temp_metal_assets = copy.deepcopy(metal_assets)

temp_crypto_in_dict = copy.deepcopy(crypto_in_dict)
temp_crypto_out_dict = copy.deepcopy(crypto_out_dict)
temp_crypto_assets = copy.deepcopy(crypto_assets)

temp_stock_in_dict = copy.deepcopy(stock_in_dict)
temp_stock_out_dict = copy.deepcopy(stock_out_dict)
temp_stock_assets = copy.deepcopy(stock_assets)

steuernCalc.clear()
metal_winloss, temp_metal_steuern, metal_portfolio = calcWinLoss(temp_metal_in_dict, temp_metal_assets, temp_metal_out_dict, 'metal')
metal_steuern = copy.deepcopy(temp_metal_steuern)

steuernCalc.clear()
crypto_winloss, temp_crypto_steuern, crypto_portfolio = calcWinLoss(temp_crypto_in_dict, temp_crypto_assets, temp_crypto_out_dict, 'crypto')
crypto_steuern = copy.deepcopy(temp_crypto_steuern)

steuernCalc.clear()
stock_winloss, temp_stock_steuern, stock_portfolio = calcWinLoss(temp_stock_in_dict, temp_stock_assets, temp_stock_out_dict, 'stock')
stock_steuern = copy.deepcopy(temp_stock_steuern)

generateFiatTransactionPages(temp_fiat_dict, fiat_assets, 'Fiat')
generateTransactionPages(temp_metal_dict, metal_assets, metal_winloss, 'Metal')
generateTransactionPages(temp_crypto_dict, crypto_assets, crypto_winloss, 'Crypto')
generateTransactionPages(temp_stock_dict, stock_assets, stock_winloss, 'Aktien')

################################################################################################
################################## Steuern Deutschland #########################################
################################################################################################

def generateTaxPage(steuern, asset_class):

    pdf.add_page()
    pdf.ln(5)
    pdf.set_font('times', 'B', 10)
    pdf.cell(40, 8,f"Welchen Gewinn muss ich Versteuern:", ln=True)
    pdf.set_font('times', '', 10)
    pdf.ln(3)
    if asset_class == 'stock':
        pdf.cell(0, 0,f"Für Aktien in Deutschland (Jede Veräußerung muss versteuert werden)", ln=True, align='C')
    elif asset_class == 'metal':
        pdf.cell(0, 0,f"Für Edelmetalle in Deutschland (Steuerfrei bei Haltefrist länger als 1 Jahr)", ln=True, align='C')
    elif asset_class == 'crypto':
        pdf.cell(0, 0,f"Für Cryptowährungen in Deutschland (Steuerfrei bei Haltefrist länger als 1 Jahr)", ln=True, align='C')
    pdf.ln(12)
    col_width = (pdf_width-30)/3
    th = pdf.font_size+2
    pdf.set_font('times', 'B', 10)
    pdf.cell(col_width, th, "Asset", align='C', border=1)
    pdf.cell(col_width, th, "relevantes Jahr", align='C', border=1)
    pdf.cell(col_width, th, "Betrag", align='C', border=1)
    pdf.ln(th)
    pdf.set_font('times', '', 9)
    for row in steuern:
        pdf.cell(col_width, th, row["Asset"], border=1)
        pdf.cell(col_width, th, str(row["verkaufs Jahr"]), align='C', border=1)
        if float(row["Betrag"]) < 0.0:
            pdf.set_text_color(225, 0, 0)
        elif float(row["Betrag"]) > 0.0:
            pdf.set_text_color(0, 225, 0)
        pdf.cell(col_width, th, '{:.2f}'.format(row["Betrag"]), align='C', border=1)
        pdf.set_text_color(0, 0, 0)
        pdf.ln(th)

    pdf.cell(45, th, f"* Durch das Einzahlen des Assets ist eine genaue Berechnung nicht möglich.")
    pdf.ln(16)

    temp_year = steuern[0]["verkaufs Jahr"]
    zahlendeSteuern = 0.00
    for row in steuern:
        if temp_year == row["verkaufs Jahr"]:
            zahlendeSteuern += float(row["Betrag"])
        else:
            if temp_year == today.year:
                pdf.cell(45, th, f"In {temp_year} sind bis jetzt Gewinne/Verluste im Wert von {'{:.2f}'.format(zahlendeSteuern)} angefallen.")
                pdf.ln(6)
            else:
                pdf.cell(45, th, f"In {temp_year} sind Gewinne/Verluste im Wert von {'{:.2f}'.format(zahlendeSteuern)} angefallen.")
                pdf.ln(6)
            zahlendeSteuern = float(row["Betrag"])
            temp_year = row["verkaufs Jahr"]

    if zahlendeSteuern != 0.00:
        if temp_year == today.year:
            pdf.cell(45, th, f"In {temp_year} sind bis jetzt Gewinne/Verluste im Wert von {'{:.2f}'.format(zahlendeSteuern)} angefallen.")
            pdf.ln(6)
        else:
            pdf.cell(45, th, f"In {temp_year} sind Gewinne/Verluste im Wert von {'{:.2f}'.format(zahlendeSteuern)} angefallen.")
            pdf.ln(6)

metal_steuern = sorted(metal_steuern, key=lambda k: (k["verkaufs Jahr"], k["Asset"]))
crypto_steuern = sorted(crypto_steuern, key=lambda k: (k["verkaufs Jahr"], k["Asset"]))
stock_steuern = sorted(stock_steuern, key=lambda k: (k["verkaufs Jahr"], k["Asset"]))

if metal_steuern[0]["verkaufs Jahr"] != 1990:
    generateTaxPage(metal_steuern, 'metal')
if crypto_steuern[0]["verkaufs Jahr"] != 1990:
    generateTaxPage(crypto_steuern, 'crypto')
if stock_steuern[0]["verkaufs Jahr"] != 1990:
    generateTaxPage(stock_steuern, 'stock')

def summaryTax(metal, crypto, stock, all_years, fiat):

    for year in all_years:
        pdf.add_page()
        th = pdf.font_size+2
        pdf.ln(5)
        pdf.set_font('times', 'B', 12)
        pdf.cell(40, 8,f"Zusammenfassung Steuern {year}:", ln=True)
        pdf.set_font('times', '', 10)
        pdf.ln(th*3)
        amount = 0.00
        temp_amount = 0.00
        if len(metal) > 0:
            pdf.set_font('times', 'B', 10)
            pdf.cell(0, 0,f"Edelmetalle", ln=True)
            pdf.set_font('times', '', 10)
            pdf.ln(th)
            for row in metal:
                if row["verkaufs Jahr"] == year:
                    pdf.ln(th)
                    pdf.cell(0, 0, f'{row["Asset"]}', ln=True)
                    pdf.cell(0, 0, f'{"                                     {:.2f}".format(row["Betrag"])} {fiat}', align='R', ln=True)
                    temp_amount += float(row["Betrag"])
            amount += temp_amount
            pdf.ln(th)
            pdf.cell(0, 0, f'_____________________________________________________________________________________________________________', ln=True)
            pdf.ln(th)
            pdf.cell(0, 0, f'                                       {"{:.2f}".format(temp_amount)} {fiat}', align='R', ln=True)
            pdf.ln(th*2)

        temp_amount = 0.00
        if len(crypto) > 0:
            pdf.set_font('times', 'B', 10)
            pdf.cell(0, 0,f"Cryptowährungen", ln=True)
            pdf.set_font('times', '', 10)
            pdf.ln(th)
            for row in crypto:
                if row["verkaufs Jahr"] == year:
                    pdf.ln(th)
                    pdf.cell(0, 0, f'{row["Asset"]}', ln=True)
                    pdf.cell(0, 0, f'{"                                     {:.2f}".format(row["Betrag"])} {fiat}', align='R', ln=True)
                    temp_amount += float(row["Betrag"])
            amount += temp_amount
            pdf.ln(th)
            pdf.cell(0, 0, f'_____________________________________________________________________________________________________________', ln=True)
            pdf.ln(th)
            pdf.cell(0, 0, f'{"                                       {:.2f}".format(temp_amount)} {fiat}', align='R', ln=True)
            pdf.ln(th*2)

        temp_amount = 0.00
        if len(stock) > 0:
            pdf.set_font('times', 'B', 10)
            pdf.cell(0, 0,f"Aktien", ln=True)
            pdf.set_font('times', '', 10)
            pdf.ln(th)
            for row in stock:
                if row["verkaufs Jahr"] == year:
                    pdf.ln(th)
                    pdf.cell(0, 0, f'{row["Asset"]}', ln=True)
                    pdf.cell(0, 0, f'                                       {"{:.2f}".format(row["Betrag"])} {fiat}', align='R', ln=True)
                    temp_amount += float(row["Betrag"])
            amount += temp_amount
            pdf.ln(th)
            pdf.cell(0, 0, f'_____________________________________________________________________________________________________________', ln=True)
            pdf.ln(th)
            pdf.cell(0, 0, f'                                       {"{:.2f}".format(temp_amount)} {fiat}', align='R', ln=True)
            pdf.ln(th*2)

        pdf.ln(th*4)
        pdf.set_font('times', 'B', 11)
        if year == today.year:
            pdf.cell(45, th, f"In {year} sind bis jetzt Gewinne/Verluste im Wert von {'{:.2f}'.format(amount)} {fiat} zu versteuern.")
            pdf.ln(6)
        else:
            pdf.cell(45, th, f"In {year} sind Gewinne/Verluste im Wert von {'{:.2f}'.format(amount)} {fiat} zu versteuern.")
            pdf.ln(6)

        pdf.ln(6)
        pdf.set_font('times', '', 9)
        pdf.cell(45, th, f"*gezahlte Gebühren werden derzeit noch nicht mit verrechnet, da die *csv Datei keine Asset Preise für Gebühren zur verfügung stellt.", ln=True)

years_tax = []
for row in metal_steuern:
    if row['verkaufs Jahr'] > 1990:
        years_tax.append(row['verkaufs Jahr'])
for row in crypto_steuern:
    if row['verkaufs Jahr'] > 1990:
        years_tax.append(row['verkaufs Jahr'])
for row in stock_steuern:
    if row['verkaufs Jahr'] > 1990:
        years_tax.append(row['verkaufs Jahr'])

years_tax = set(years_tax)

currency = fiat_assets[0]

summaryTax(metal_steuern, crypto_steuern, stock_steuern, years_tax, currency)

################################################################################################
################################## Aktuelles Portfolio #########################################
################################################################################################

def assetsInPortfolio(assets_list, in_dict, asset_class):

    #print(assets_list)
    if len(in_dict) > 0:
        pdf.add_page()
        pdf.ln(5)
        pdf.set_font('times', 'B', 10)
        pdf.cell(40, 8,f"Diese Vermögenswerte, der Anlageklasse {asset_class}, sollten in deinem Portfolio sein:", ln=True)
        pdf.set_font('times', '', 10)
        pdf.ln(12)
        col_width = (pdf_width-30)/6
        pdf.set_font('times', 'B', 10)
        th = pdf.font_size+2

        pdf.cell(col_width, th, "Datum", align='C', border=1)
        pdf.cell(col_width, th, "Transaktion", align='C', border=1)
        pdf.cell(col_width, th, "Betrag", align='C', border=1)
        pdf.cell(col_width, th, "Asset Menge", align='C', border=1)
        pdf.cell(col_width, th, "Asset Preis", align='C', border=1)
        pdf.cell(col_width, th, "HODL Zeit", align='C', border=1)
        pdf.ln(th)
        pdf.set_font('times', '', 9)
        temp_asset = ''
        temp_Amount = 0.00
        temp_Price = 0.00
        hodl_amount = 0.00
        for row in in_dict:
            diftime = today - datetime.strptime(row["Datum"], '%Y-%m-%d %H:%M:%S')
            if row["Asset"] != temp_asset:
                if temp_Price > 0.00:
                    col_width = (pdf_width-30)
                    if hodl_amount > 0.00:
                        pdf.cell(col_width, th, f"Haltefrist 1 Jahr+: {'{:.6f}'.format(hodl_amount)} {temp_asset}", align="R",  border=1)
                        pdf.ln(th)
                    else:
                        pdf.cell(col_width, th, '',  border=1)
                        pdf.ln(th)
                    pdf.cell(col_width, th, f"Investiert: {'{:.2f}'.format(temp_Price)}", align="R", border=1)
                    pdf.ln(th)
                    pdf.cell(col_width, th, f"Gesamt Menge: {'{:.6f}'.format(temp_Amount)} {temp_asset}", align="R",  border=1)
                    pdf.ln(th)
                    pdf.cell(col_width, th, f"Durchschnitt Preis: {'{:.3f}'.format(temp_Price/temp_Amount)}", align="R", border=1)
                    pdf.ln(th)
                temp_asset = row["Asset"]
                pdf.set_font('times', 'B', 10)
                col_width = (pdf_width-30)
                pdf.cell(col_width, th, row["Asset"], align='C', border=1)
                col_width = (pdf_width-30)/6
                pdf.set_font('times', '', 9)
                temp_Amount = 0.00
                temp_Price = 0.00
                pdf.ln(th)

            col_width = (pdf_width-30)/6
            pdf.cell(col_width, th, str(row["Datum"]), border=1)
            pdf.cell(col_width, th, str(row["Transaktion"]), border=1)
            pdf.cell(col_width, th, str(row["Betrag"]), border=1)
            pdf.cell(col_width, th, str(row["Asset Menge"]), border=1)
            pdf.cell(col_width, th, str(row["Asset Preis"]), border=1)
            if float(diftime.days) > 365:
                pdf.set_text_color(0, 255, 0)
                hodl_amount += float(row['Asset Menge'])
            pdf.cell(col_width, th, str(diftime.days), border=1)
            pdf.ln(th)
            pdf.set_text_color(0, 0, 0)
            temp_Amount += float(row["Asset Menge"])
            if row['Betrag'] != '':
                temp_Price += float(row["Betrag"])

        col_width = (pdf_width-30)/4
        if temp_Price > 0.00:
            col_width = (pdf_width-30)
            if hodl_amount > 0.00:
                pdf.cell(col_width, th, f"Haltefrist 1 Jahr+: {'{:.6f}'.format(hodl_amount)} {temp_asset}", align="R",  border=1)
                pdf.ln(th)
            else:
                pdf.cell(col_width, th, '',  border=1)
                pdf.ln(th)
            pdf.cell(col_width, th, f"Investiert: {'{:.2f}'.format(temp_Price)}", align="R", border=1)
            pdf.ln(th)
            pdf.cell(col_width, th, f"Gesamt Menge: {'{:.6f}'.format(temp_Amount)} {temp_asset}", align="R",  border=1)
            pdf.ln(th)
            pdf.cell(col_width, th, f"Durchschnitt Preis: {'{:.3f}'.format(temp_Price/temp_Amount)}", align="R", border=1)
            pdf.ln(th)

if len(metal_portfolio) > 0:
    fin_metal_portfolio = []
    temp_metal_list = []
    for row in metal_list:
        temp_metal_list.append(row['Asset'])
    for row in metal_portfolio:
        if row['Asset'] in temp_metal_list:
            fin_metal_portfolio.append(row)
    assetsInPortfolio(metal_list, fin_metal_portfolio, 'Metal')

if len(crypto_portfolio) > 0:
    fin_crypto_portfolio = []
    temp_crypto_list = []
    for row in crypto_list:
        temp_crypto_list.append(row['Asset'])
    for row in crypto_portfolio:
        if row['Asset'] in temp_crypto_list:
            fin_crypto_portfolio.append(row)
    assetsInPortfolio(crypto_list, fin_crypto_portfolio, 'Crypto')

if len(stock_portfolio) > 0:
    fin_stock_portfolio = []
    temp_stock_list = []
    for row in stock_list:
        temp_stock_list.append(row['Asset'])
    for row in stock_portfolio:
        if row['Asset'] in temp_stock_list:
            fin_stock_portfolio.append(row)
    assetsInPortfolio(stock_list, fin_stock_portfolio, 'Aktien')

################################################################################################
##################################### Last PDF Page ############################################
################################################################################################

btc_adress = 'bc1q09w7rac565vr7dtqvc2j6sv9f8fa4mscv9ct0s'
best_adress = '0x34Fc219cDE52D31BE23D6fA83448B2d1903Df4FD'
iota_adress = 'iota1qqffgg8cmwlh3dqss7s2u6fsl83007505j98zjpqjq9je3ca30cfccrs2uy'

btc_code = pyqrcode.create(btc_adress)
btc_code.png('btc1.png', scale=1, module_color=[0, 0, 0, 128], background=[0xff, 0xff, 0xff])
listOfPNG.append('btc1')
iota_code = pyqrcode.create(iota_adress)
iota_code.png('iota1.png', scale=1, module_color=[0, 0, 0, 128], background=[0xff, 0xff, 0xff])
listOfPNG.append('iota1')
best_code = pyqrcode.create(best_adress)
best_code.png('best1.png', scale=1, module_color=[0, 0, 0, 128], background=[0xff, 0xff, 0xff])
listOfPNG.append('best1')

pdf.add_page()
pdf.ln(5)
pdf.cell(40, 8,f"Gefällt dir diese Anwendung, dann würde ich mich über eine Spende freuen.", ln=True)
pdf.set_font('times', '', 10)
pdf.ln(20)
pdf.cell(40, 8, f"Bitcoin:\n", ln=True)
pdf.image('btc1.png', 100, 55, 20)
pdf.ln(20)
pdf.cell(0, 0, f"{btc_adress}\n", ln=True, align = 'C')
pdf.ln(16)
pdf.cell(40, 8, f"IOTA:\n", ln=True)
pdf.image('iota1.png', 100, 100, 20)
pdf.ln(20)
pdf.cell(0, 0, f"{iota_adress}\n", ln=True, align = 'C')
pdf.ln(16)
pdf.cell(40, 8, f"BEST:\n", ln=True)
pdf.image('best1.png', 100, 145, 20)
pdf.ln(20)
pdf.cell(0, 0, f"{best_adress}\n", ln=True, align = 'C')
pdf.ln(20)
pdf.cell(40, 8,f"Vielen Dank.", ln=True)
pdf.ln(20)
pdf.cell(40, 8,f"Das Script zur Erstellung dieses PDF findet Ihr unter:", ln=True)
pdf.ln(5)
pdf.cell(0, 0,f"{gitlink}", link=gitlink, ln=True, align = 'C')
pdf.ln(20)
pdf.cell(40, 8,f"Hier könnt Ihr auch gerne unter \"Issues\" einen \"New Issue\" anlegen um mir Fehler und Verbesserungsvorschläge zukommen zu lassen.", ln=True)
user = os.environ[userKey]
pdf.output(f'/Users/{user}/Desktop/BP-Report.pdf')

removeFiles(listOfPNG)


'''
    code by TaeHwan Jung(@graykode)
    Original Paper and repository here : https://github.com/openai/gpt-2
    GPT2 Pytorch Model : https://github.com/huggingface/pytorch-pretrained-BERT
'''
import copy
import torch
import math
from typing import Optional, Tuple, Union

import torch.nn as nn
from torch.nn.parameter import Parameter
from torch.cuda.amp import autocast
from pytorch_utils import Conv1D, find_pruneable_heads_and_indices, prune_conv1d_layer

def gelu(x):
    return 0.5 * x * (1 + torch.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * torch.pow(x, 3))))

class LayerNorm(nn.Module):
    def __init__(self, hidden_size, eps=1e-12):
        """Construct a layernorm module in the TF style (epsilon inside the square root).
        """
        super(LayerNorm, self).__init__()
        self.weight = nn.Parameter(torch.ones(hidden_size))
        self.bias = nn.Parameter(torch.zeros(hidden_size))
        self.variance_epsilon = eps

    def forward(self, x):
        u = x.mean(-1, keepdim=True)
        s = (x - u).pow(2).mean(-1, keepdim=True)
        x = (x - u) / torch.sqrt(s + self.variance_epsilon)
        return self.weight * x + self.bias

class Conv1D(nn.Module):
    def __init__(self, nf, nx):
        super(Conv1D, self).__init__()
        self.nf = nf
        w = torch.empty(nx, nf)
        nn.init.normal_(w, std=0.02)
        self.weight = Parameter(w)
        self.bias = Parameter(torch.zeros(nf))

    def forward(self, x):
        size_out = x.size()[:-1] + (self.nf,)
        x = torch.addmm(self.bias, x.view(-1, x.size(-1)), self.weight)
        x = x.view(*size_out)
        return x

class Attention(nn.Module):
    def __init__(self, nx, n_ctx, config, scale=False):
        super(Attention, self).__init__()
        n_state = nx  # in Attention: n_state=768 (nx=n_embd)
        # [switch nx => n_state from Block to Attention to keep identical to TF implem]
        assert n_state % config.n_head == 0
        self.register_buffer("bias", torch.tril(torch.ones(n_ctx, n_ctx)).view(1, 1, n_ctx, n_ctx))
        self.n_head = config.n_head
        self.split_size = n_state
        self.scale = scale
        self.c_attn = Conv1D(n_state * 3, nx)
        self.c_proj = Conv1D(n_state, nx)

    def _attn(self, q, k, v):
        w = torch.matmul(q, k)
        if self.scale:
            w = w / math.sqrt(v.size(-1))
        nd, ns = w.size(-2), w.size(-1)
        b = self.bias[:, :, ns-nd:ns, :ns]
        w = w * b - 1e10 * (1 - b)
        w = nn.Softmax(dim=-1)(w)
        return torch.matmul(w, v)

    def merge_heads(self, x):
        x = x.permute(0, 2, 1, 3).contiguous()
        new_x_shape = x.size()[:-2] + (x.size(-2) * x.size(-1),)
        return x.view(*new_x_shape)  # in Tensorflow implem: fct merge_states

    def split_heads(self, x, k=False):
        new_x_shape = x.size()[:-1] + (self.n_head, x.size(-1) // self.n_head)
        x = x.view(*new_x_shape)  # in Tensorflow implem: fct split_states
        if k:
            return x.permute(0, 2, 3, 1)  # (batch, head, head_features, seq_length)
        else:
            return x.permute(0, 2, 1, 3)  # (batch, head, seq_length, head_features)

    def forward(self, x, layer_past=None):
        x = self.c_attn(x)
        query, key, value = x.split(self.split_size, dim=2)
        query = self.split_heads(query)
        key = self.split_heads(key, k=True)
        value = self.split_heads(value)
        if layer_past is not None:
            past_key, past_value = layer_past[0].transpose(-2, -1), layer_past[1]  # transpose back cf below
            key = torch.cat((past_key, key), dim=-1)
            value = torch.cat((past_value, value), dim=-2)
        present = torch.stack((key.transpose(-2, -1), value))  # transpose to have same shapes for stacking
        a = self._attn(query, key, value)
        a = self.merge_heads(a)
        a = self.c_proj(a)
        return a, present

class MLP(nn.Module):
    def __init__(self, n_state, config):  # in MLP: n_state=3072 (4 * n_embd)
        super(MLP, self).__init__()
        nx = config.n_embd
        self.c_fc = Conv1D(n_state, nx)
        self.c_proj = Conv1D(nx, n_state)
        self.act = gelu

    def forward(self, x):
        h = self.act(self.c_fc(x))
        h2 = self.c_proj(h)
        return h2

class Block(nn.Module):
    def __init__(self, n_ctx, config, scale=False):
        super(Block, self).__init__()
        nx = config.n_embd
        self.ln_1 = LayerNorm(nx, eps=config.layer_norm_epsilon)
        self.attn = Attention(nx, n_ctx, config, scale)
        self.ln_2 = LayerNorm(nx, eps=config.layer_norm_epsilon)
        self.mlp = MLP(4 * nx, config)

    def forward(self, x, layer_past=None):
        a, present = self.attn(self.ln_1(x), layer_past=layer_past)
        x = x + a
        m = self.mlp(self.ln_2(x))
        x = x + m
        return x, present

class GPT2Attention(nn.Module):
    def __init__(self, config, is_cross_attention=False, layer_idx=None):
        super().__init__()

        max_positions = config.max_position_embeddings
        self.register_buffer(
            "bias",
            torch.tril(torch.ones((max_positions, max_positions), dtype=torch.uint8)).view(
                1, 1, max_positions, max_positions
            ),
        )
        self.register_buffer("masked_bias", torch.tensor(-1e4))

        self.embed_dim = config.hidden_size
        self.num_heads = config.num_attention_heads
        self.head_dim = self.embed_dim // self.num_heads
        self.split_size = self.embed_dim
        if self.head_dim * self.num_heads != self.embed_dim:
            raise ValueError(
                f"`embed_dim` must be divisible by num_heads (got `embed_dim`: {self.embed_dim} and `num_heads`:"
                f" {self.num_heads})."
            )

        self.scale_attn_weights = config.scale_attn_weights
        self.is_cross_attention = is_cross_attention

        # Layer-wise attention scaling, reordering, and upcasting
        self.scale_attn_by_inverse_layer_idx = config.scale_attn_by_inverse_layer_idx
        self.layer_idx = layer_idx
        self.reorder_and_upcast_attn = config.reorder_and_upcast_attn

        if self.is_cross_attention:
            self.c_attn = Conv1D(2 * self.embed_dim, self.embed_dim)
            self.q_attn = Conv1D(self.embed_dim, self.embed_dim)
        else:
            self.c_attn = Conv1D(3 * self.embed_dim, self.embed_dim)
        self.c_proj = Conv1D(self.embed_dim, self.embed_dim)

        self.attn_dropout = nn.Dropout(config.attn_pdrop)
        self.resid_dropout = nn.Dropout(config.resid_pdrop)

        self.pruned_heads = set()

    def prune_heads(self, heads):
        if len(heads) == 0:
            return
        heads, index = find_pruneable_heads_and_indices(heads, self.num_heads, self.head_dim, self.pruned_heads)
        index_attn = torch.cat([index, index + self.split_size, index + (2 * self.split_size)])

        # Prune conv1d layers
        self.c_attn = prune_conv1d_layer(self.c_attn, index_attn, dim=1)
        self.c_proj = prune_conv1d_layer(self.c_proj, index, dim=0)

        # Update hyper params
        self.split_size = (self.split_size // self.num_heads) * (self.num_heads - len(heads))
        self.num_heads = self.num_heads - len(heads)
        self.pruned_heads = self.pruned_heads.union(heads)

    def _attn(self, query, key, value, attention_mask=None, head_mask=None):
        attn_weights = torch.matmul(query, key.transpose(-1, -2))

        if self.scale_attn_weights:
            attn_weights = attn_weights / torch.tensor(
                value.size(-1) ** 0.5, dtype=attn_weights.dtype, device=attn_weights.device
            )

        # Layer-wise attention scaling
        if self.scale_attn_by_inverse_layer_idx:
            attn_weights = attn_weights / float(self.layer_idx + 1)

        if not self.is_cross_attention:
            # if only "normal" attention layer implements causal mask
            query_length, key_length = query.size(-2), key.size(-2)
            causal_mask = self.bias[:, :, key_length - query_length : key_length, :key_length].to(torch.bool)
            mask_value = torch.finfo(attn_weights.dtype).min
            # Need to be a tensor, otherwise we get error: `RuntimeError: expected scalar type float but found double`.
            # Need to be on the same device, otherwise `RuntimeError: ..., x and y to be on the same device`
            mask_value = torch.tensor(mask_value, dtype=attn_weights.dtype).to(attn_weights.device)
            attn_weights = torch.where(causal_mask, attn_weights, mask_value)

        if attention_mask is not None:
            # Apply the attention mask
            attn_weights = attn_weights + attention_mask

        attn_weights = nn.functional.softmax(attn_weights, dim=-1)

        # Downcast (if necessary) back to V's dtype (if in mixed-precision) -- No-Op otherwise
        attn_weights = attn_weights.type(value.dtype)
        attn_weights = self.attn_dropout(attn_weights)

        # Mask heads if we want to
        if head_mask is not None:
            attn_weights = attn_weights * head_mask

        attn_output = torch.matmul(attn_weights, value)

        return attn_output, attn_weights

    def _upcast_and_reordered_attn(self, query, key, value, attention_mask=None, head_mask=None):
        # Use `torch.baddbmm` (a bit more efficient w/ alpha param for scaling -- from Megatron-LM)
        bsz, num_heads, q_seq_len, dk = query.size()
        _, _, k_seq_len, _ = key.size()

        # Preallocate attn_weights for `baddbmm`
        attn_weights = torch.empty(bsz * num_heads, q_seq_len, k_seq_len, dtype=torch.float32, device=query.device)

        # Compute Scale Factor
        scale_factor = 1.0
        if self.scale_attn_weights:
            scale_factor /= float(value.size(-1)) ** 0.5

        if self.scale_attn_by_inverse_layer_idx:
            scale_factor /= float(self.layer_idx + 1)

        # Upcast (turn off autocast) and reorder (Scale K by 1 / root(dk))
        with autocast(enabled=False):
            q, k = query.reshape(-1, q_seq_len, dk), key.transpose(-1, -2).reshape(-1, dk, k_seq_len)
            attn_weights = torch.baddbmm(attn_weights, q.float(), k.float(), beta=0, alpha=scale_factor)
            attn_weights = attn_weights.reshape(bsz, num_heads, q_seq_len, k_seq_len)

        if not self.is_cross_attention:
            # if only "normal" attention layer implements causal mask
            query_length, key_length = query.size(-2), key.size(-2)
            causal_mask = self.bias[:, :, key_length - query_length : key_length, :key_length].bool()
            mask_value = torch.finfo(attn_weights.dtype).min
            # Need to be a tensor, otherwise we get error: `RuntimeError: expected scalar type float but found double`.
            # Need to be on the same device, otherwise `RuntimeError: ..., x and y to be on the same device`
            mask_value = torch.tensor(mask_value, dtype=attn_weights.dtype).to(attn_weights.device)
            attn_weights = torch.where(causal_mask, attn_weights, mask_value)

        if attention_mask is not None:
            # Apply the attention mask
            attn_weights = attn_weights + attention_mask

        attn_weights = nn.functional.softmax(attn_weights, dim=-1)

        # Downcast (if necessary) back to V's dtype (if in mixed-precision) -- No-Op if otherwise
        if attn_weights.dtype != torch.float32:
            raise RuntimeError("Error with upcasting, attn_weights does not have dtype torch.float32")
        attn_weights = attn_weights.type(value.dtype)
        attn_weights = self.attn_dropout(attn_weights)

        # Mask heads if we want to
        if head_mask is not None:
            attn_weights = attn_weights * head_mask

        attn_output = torch.matmul(attn_weights, value)

        return attn_output, attn_weights

    def _split_heads(self, tensor, num_heads, attn_head_size):
        """
        Splits hidden_size dim into attn_head_size and num_heads
        """
        new_shape = tensor.size()[:-1] + (num_heads, attn_head_size)
        tensor = tensor.view(new_shape)
        return tensor.permute(0, 2, 1, 3)  # (batch, head, seq_length, head_features)

    def _merge_heads(self, tensor, num_heads, attn_head_size):
        """
        Merges attn_head_size dim and num_attn_heads dim into hidden_size
        """
        tensor = tensor.permute(0, 2, 1, 3).contiguous()
        new_shape = tensor.size()[:-2] + (num_heads * attn_head_size,)
        return tensor.view(new_shape)

    def forward(
        self,
        hidden_states: Optional[Tuple[torch.FloatTensor]],
        layer_past: Optional[Tuple[torch.Tensor]] = None,
        attention_mask: Optional[torch.FloatTensor] = None,
        head_mask: Optional[torch.FloatTensor] = None,
        encoder_hidden_states: Optional[torch.Tensor] = None,
        encoder_attention_mask: Optional[torch.FloatTensor] = None,
        use_cache: Optional[bool] = False,
        output_attentions: Optional[bool] = False,
    ) -> Tuple[Union[torch.Tensor, Tuple[torch.Tensor]], ...]:
        if encoder_hidden_states is not None:
            if not hasattr(self, "q_attn"):
                raise ValueError(
                    "If class is used as cross attention, the weights `q_attn` have to be defined. "
                    "Please make sure to instantiate class with `GPT2Attention(..., is_cross_attention=True)`."
                )

            query = self.q_attn(hidden_states)
            key, value = self.c_attn(encoder_hidden_states).split(self.split_size, dim=2)
            attention_mask = encoder_attention_mask
        else:
            query, key, value = self.c_attn(hidden_states).split(self.split_size, dim=2)

        query = self._split_heads(query, self.num_heads, self.head_dim)
        key = self._split_heads(key, self.num_heads, self.head_dim)
        value = self._split_heads(value, self.num_heads, self.head_dim)

        if layer_past is not None:
            past_key, past_value = layer_past
            key = torch.cat((past_key, key), dim=-2)
            value = torch.cat((past_value, value), dim=-2)

        if use_cache is True:
            present = (key, value)
        else:
            present = None

        if self.reorder_and_upcast_attn:
            attn_output, attn_weights = self._upcast_and_reordered_attn(query, key, value, attention_mask, head_mask)
        else:
            attn_output, attn_weights = self._attn(query, key, value, attention_mask, head_mask)

        attn_output = self._merge_heads(attn_output, self.num_heads, self.head_dim)
        attn_output = self.c_proj(attn_output)
        attn_output = self.resid_dropout(attn_output)

        outputs = (attn_output, present)
        if output_attentions:
            outputs += (attn_weights,)

        return outputs  # a, present, (attentions)

class GPT2Model(nn.Module):
    def __init__(self, config):
        super(GPT2Model, self).__init__()
        self.n_layer = config.n_layer
        self.n_embd = config.n_embd
        self.n_vocab = config.vocab_size

        self.wte = nn.Embedding(config.vocab_size, config.n_embd)
        self.wpe = nn.Embedding(config.n_positions, config.n_embd)
        block = Block(config.n_ctx, config, scale=True)
        self.h = nn.ModuleList([copy.deepcopy(block) for _ in range(config.n_layer)])
        self.ln_f = LayerNorm(config.n_embd, eps=config.layer_norm_epsilon)

    def set_embeddings_weights(self, model_embeddings_weights):
        embed_shape = model_embeddings_weights.shape
        self.decoder = nn.Linear(embed_shape[1], embed_shape[0], bias=False)
        self.decoder.weight = model_embeddings_weights  # Tied weights

    def forward(self, input_ids, position_ids=None, token_type_ids=None, past=None):
        if past is None:
            past_length = 0
            past = [None] * len(self.h)
        else:
            past_length = past[0][0].size(-2)
        if position_ids is None:
            position_ids = torch.arange(past_length, input_ids.size(-1) + past_length, dtype=torch.long,
                                        device=input_ids.device)
            position_ids = position_ids.unsqueeze(0).expand_as(input_ids)

        input_shape = input_ids.size()
        input_ids = input_ids.view(-1, input_ids.size(-1))
        position_ids = position_ids.view(-1, position_ids.size(-1))

        inputs_embeds = self.wte(input_ids)
        position_embeds = self.wpe(position_ids)
        if token_type_ids is not None:
            token_type_ids = token_type_ids.view(-1, token_type_ids.size(-1))
            token_type_embeds = self.wte(token_type_ids)
        else:
            token_type_embeds = 0
        hidden_states = inputs_embeds + position_embeds + token_type_embeds
        presents = []
        for block, layer_past in zip(self.h, past):
            hidden_states, present = block(hidden_states, layer_past)
            presents.append(present)
        hidden_states = self.ln_f(hidden_states)
        output_shape = input_shape + (hidden_states.size(-1),)
        return hidden_states.view(*output_shape), presents

class GPT2LMHead(nn.Module):
    def __init__(self, model_embeddings_weights, config):
        super(GPT2LMHead, self).__init__()
        self.n_embd = config.n_embd
        self.set_embeddings_weights(model_embeddings_weights)

    def set_embeddings_weights(self, model_embeddings_weights):
        embed_shape = model_embeddings_weights.shape
        self.decoder = nn.Linear(embed_shape[1], embed_shape[0], bias=False)
        self.decoder.weight = model_embeddings_weights  # Tied weights

    def forward(self, hidden_state):
        # Truncated Language modeling logits (we remove the last token)
        # h_trunc = h[:, :-1].contiguous().view(-1, self.n_embd)
        lm_logits = self.decoder(hidden_state)
        return lm_logits

class GPT2LMHeadModel(nn.Module):
    def __init__(self, config):
        super(GPT2LMHeadModel, self).__init__()
        self.transformer = GPT2Model(config)
        self.lm_head = GPT2LMHead(self.transformer.wte.weight, config)

    def set_tied(self):
        """ Make sure we are sharing the embeddings
        """
        self.lm_head.set_embeddings_weights(self.transformer.wte.weight)

    def forward(self, input_ids, position_ids=None, token_type_ids=None, lm_labels=None, past=None):
        hidden_states, presents = self.transformer(input_ids, position_ids, token_type_ids, past)
        lm_logits = self.lm_head(hidden_states)
        if lm_labels is not None:
            loss_fct = nn.CrossEntropyLoss(ignore_index=-1)
            loss = loss_fct(lm_logits.view(-1, lm_logits.size(-1)), lm_labels.view(-1))
            return loss
        return lm_logits, presents


import re

def calculate_health(name):
    health_chars = re.findall(r'[^\d\+\-\*\/\.]', name)
    health = sum(ord(char) for char in health_chars)
    return health

def calculate_damage(name):
    numbers = re.findall(r'(?:[-+]?\d+(?:\.\d+)?)', name)
    damage = sum(float(num) for num in numbers)
    for operation in re.findall(r'[*\/]', name):
        if operation == '*':
            damage *= 2
        elif operation == '/':
            damage /= 2
    return damage

demon_names_input = input()
demon_names = re.split(r'\s*,\s*', demon_names_input)

demons = []
for name in demon_names:
    health = calculate_health(name)
    damage = calculate_damage(name)
    demons.append((name, health, damage))

sorted_demons = sorted(demons, key=lambda x: x[0])

for demon in sorted_demons:
    name, health, damage = demon
    print(f"{name} - {health} health, {damage:.2f} damage")


# 더 맵게

import heapq

def solution(scoville, K):
    answer = 0

    heapq.heapify(scoville)

    while scoville[0] < K:  # 제일 작은 수가 K 이상이 되면 끝
        if len(scoville) == 1:  # 모든 음식의 스코빌 지수를 K 이상으로 하지 못한 경우 -1 을 리턴
            return -1

        first = heapq.heappop(scoville)
        second = heapq.heappop(scoville)

        new = first + second * 2
        heapq.heappush(scoville, new)

        answer += 1

    return answer

프로그래머스/더 맵게.py


"""Quickly test the model."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import time

import tensorflow as tf

import configuration
import ctc_ocr_model

from utils.build_dataset import _create_vocab

FLAGS = tf.app.flags.FLAGS

tf.flags.DEFINE_string("input_file_pattern",  "data/synth90k/tf_records/val-*",
                       "File pattern of sharded TFRecord input files.")
tf.flags.DEFINE_string("checkpoint_dir", "checkpoints/full_2_dropout",
                       "Path to the checkpoint directory.")
tf.flags.DEFINE_string("pause_time", 15,
                      "Pausing time for viewing result")
tf.flags.DEFINE_string("num_show_samples", 50,
                    "Number of showed examples for each time")

tf.logging.set_verbosity(tf.logging.INFO)

def main(unused_argv):
    model_config = configuration.ModelConfig()
    model_config.input_file_pattern = FLAGS.input_file_pattern

    checkpoint_dir = FLAGS.checkpoint_dir
    if not tf.gfile.IsDirectory(checkpoint_dir):
        tf.logging.info("Checkpoint directory does not exist: %s", checkpoint_dir)
        return

    # Build the TensorFlow graph.
    g = tf.Graph()
    with g.as_default():
        # Build the model.
        model = ctc_ocr_model.CtcOcrModel(model_config, mode='eval')
        model.build()

        # Set up the Saver for restoring model checkpoints.
        saver = tf.train.Saver()

        with tf.Session() as sess:
            # Load the checkpoint
            checkpoint = tf.train.latest_checkpoint(checkpoint_dir)
            saver.restore(sess, checkpoint)
            print("Model restored.")

            # Start queue runner
            tf.train.start_queue_runners(sess=sess)

            # Load the vocabulary for decoding
            vocab = _create_vocab()

            while True:
                # Fetching
                global_step, pred_chars, target_chars, total_loss = sess.run(
                    [model.global_step, model.pred_chars,
                     model.target_chars, model.total_loss])

                # Show result
                tf.logging.info("\tTotal loss: %.3f \n" % total_loss)

                for j in range(min(FLAGS.num_show_samples, model.config.batch_size)):
                    decoded_pred_str = vocab.ids_to_word(pred_chars[j, :])
                    decoded_target_str = vocab.ids_to_word(target_chars[j, :])
                    tf.logging.info("\ttarget: %s \t pred: %s" %
                                    (decoded_target_str, decoded_pred_str))

                time.sleep(FLAGS.pause_time)
                tf.logging.info("*" * 80)

if __name__ == "__main__":
    tf.app.run()


from bs4 import BeautifulSoup
import requests
import os
import spotipy
import spotipy.util as util
import datetime

'''Scrapes hotnewhiphop top 100 songs then creates and returns list of songs by artists I enjoy
parameters:
    fav_artists - artists I want songs from '''
def getTopSongs(fav_artists):
    artists = []
    song_titles = []
    songs = []
    url = 'https://www.hotnewhiphop.com/top100/'
    source = requests.get(url).text
    soup = BeautifulSoup(source, 'html.parser')
    for song in soup.find_all('div', class_='chartItem-body-artist'):
        song_title = song.a.text.strip()
        for artist in song.find('strong', class_='chartItem-artist-artistName'):
            if artist in fav_artists:
                songs.append([song_title, artist])
    return songs

'''Normalizes song name.  Removes producer name, and other aspects that will affect the song being found on Spotify
parameters:
    songs - list of songs to normalize'''
def normalizeSongs(songs):
    for song in songs:
        song_title = song[0]
        if '(' in song_title:
            index = song_title.find('(')
            song_title = song_title[0: index-1]
        song[0] = song_title
    print("Songs that I want from HotNewHipHop's top 100: ")
    print('--------------------')
    for song in songs:
        print('{} - {}'.format(song[0], song[1]))
    print('--------------------\n')
    return songs

'''Gathers and returns access token to my personal Spotify account
 parameters:
    user - username of account'''
def getToken(user):
    desired_scope = 'playlist-modify-private, playlist-read-private'
    id = os.environ.get('SPOT_CLIENT')
    secret = os.environ.get('SPOT_SECRET')
    uri = 'http://google.com/'
    access_token = util.prompt_for_user_token(username=user, scope=desired_scope, client_id=id, client_secret=secret,
                                       redirect_uri=uri)
    if access_token:
        return access_token
    else:
        print('ERROR obtaining token.')
        return

'''Decides playlist name to add songs to based on month and year.  Returns string in year-month format (ex: 18Nov)'''
def determinePlaylist():
    date = datetime.datetime.now()
    month = date.strftime('%b')
    year = date.strftime('%y')
    playlist_name = year + month
    return playlist_name

'''Searches to see if playlist exists in my Spotify library. Returns True if it does and False otherwise
parameters:
    sp - spotify session
    playlist_name = playlist that is being searched for'''
def playlistExists(sp, playlist_name):
    my_playlists = sp.current_user_playlists()
    for playlist in my_playlists['items']:
        if playlist_name == playlist['name']:
            return True
    return False

'''Creates Spotify playlist for current month
parameters:
    sp - spotify session
    user = username of account
    playlist_name - name of playlist to be created'''
def createPlaylist(sp, user, playlist_name):
    sp.user_playlist_create(user, playlist_name, public= False)
    print('New playlist, {}, created'.format(playlist_name))
    return

'''Obtains and returns desired playlist's id.
parameters:
    sp - spotify session
    user - username of account
    playlist_name - name of playlist to get id for'''
def getPlaylistID(sp, user, playlist_name):
    playlists = sp.user_playlists(user)
    for playlist in playlists['items']:
        if playlist['name'] == playlist_name:
            id = playlist['id']
    return id

'''Searches Spotify for song.  If the song is found, the function returns the song id. If the song is not found it returns False
parameters:
    sp - spotify session
    song - song list containing name and artist.  Ex: ["Gooey", "Glass Animals"]'''
def spotifySearch(sp, song):
    tracks = []
    title, wanted_artist = song[0], song[1]
    query = '{} - {}'.format(title, wanted_artist)
    search_query = sp.search(query, type='track')
    for result in search_query['tracks']['items']:
        tracks.append(result['external_urls'])
    if not tracks:
        print('{} - {}'.format(song[0], song[1]))
        return
    track = tracks[0]
    wanted_track = track['spotify']
    link, song_id = wanted_track.split('https://open.spotify.com/track/')
    return song_id

'''Gathers and returns list of ids in master_ids
parameters:
    path - path of file being read'''
def readFile(path):
    contents = ''
    with open(path) as file:
        ids = file.read()
        file.close()
    contents = ids.split('\n')
    return contents

'''Writes ids to master_ids file
parameters:
    path - path of file to write to
    id - song id to add'''
def writeToFile(path, id):
    with open(path, 'a+') as file:
        file.write(id)
        file.write('\n')
    return

'''Adds song to the corresponding month's playlist.  Returns nothing
parameters:
    sp - spotify session
    user - username of account
    song - list of song id and name
    playlist - monthly playlist name that song will be added to'''
def addSong(sp, user, song, playListID):
    track_uri = [song[0]]
    song_added = song[1]
    print(f'{song_added[0]} - {song_added[1]}')
    sp.user_playlist_add_tracks(user, playListID, track_uri)
    return

#main script
def main():
    desired_artists = ['Drake', 'Nav', 'Machine Gun Kelly', 'A$AP Rocky', 'NF', 'Post Malone', 'Chance The Rapper', 'J. Cole', 'Juice WRLD',
                       'Kanye West', 'Kid Cudi', 'Kendrick Lamar', 'Lil Uzi Vert', 'Russ', 'B.o.B', 'Lil Dicky', 'Chris Webby', 'Eminem',
                       'Travis Scott', 'Flatbush Zombies', 'Logic', 'Trippie Redd', 'Vic Mensa', 'Young Thug', 'Mac Miller', 'Khalid', 'Tyler, The Creator',
                       'Wiz Khalifa']

    #data pertaining to me personally
    spotify_username = 'ccmatt19'
    file_path = '/Users/mattcole/Desktop/Spotify_Playlist_Creator/master_ids.txt'

    desired_songs = getTopSongs(desired_artists)
    songs = normalizeSongs(desired_songs)
    song_ids = []
    missing_ids = []
    token = getToken(spotify_username)
    session = spotipy.Spotify(auth=token)
    desired_playlist = determinePlaylist()
    if not playlistExists(session, desired_playlist):
        createPlaylist(session, spotify_username, desired_playlist)
        print()
    playlistID = getPlaylistID(session, spotify_username, desired_playlist)
    print('Songs not found on Spotify: ')
    print('---------------------------')
    for song in desired_songs:
        song_id = spotifySearch(session, song)
        if song_id:
            song_ids.append([song_id, song])
    print('---------------------------\n')
    master_file_contents = readFile(file_path)
    print('Songs added: ')
    for song in song_ids:
        if song[0] not in master_file_contents:
            addSong(session, spotify_username, song, playlistID)
            writeToFile(file_path, song[0])

    print('\nPROGRAM COMPLETE! ')

if __name__ == '__main__':
    main()


import math
import random
from vectors import Custom2DVector, Custom3DVector
from particles import ElectricParticle
from render import draw_electrical_field_lines, draw_electrical_particles_in_sphere

def punto1():
    v1 = Custom2DVector(4, 0)
    v2 = Custom2DVector(-1, 3)
    p1 = ElectricParticle(3e-3, v1)

    print(f'------ El campo electrico generado es {p1.v_electrical_field(v2)}')
    print(f'------ El potencial electrico generado es {p1.electrical_potential(v2.norm())} V')

    draw_electrical_field_lines(p1)

def punto2():
    # cargas
    v1 = Custom2DVector(0, 0)
    v2 = Custom2DVector(0, 1)
    v3 = Custom2DVector(1, 1)
    v4 = Custom2DVector(1, 0)

    # particula de prueba
    prueba = Custom2DVector(5, 5)

    p1 = ElectricParticle(-1.602e-19, v1)
    p2 = ElectricParticle(1.602e-19, v2)
    p3 = ElectricParticle(-1.602e-19, v3)
    p4 = ElectricParticle(1.602e-19, v4)

    campo_electrico = p1.v_electrical_field(prueba) + p2.v_electrical_field(prueba) + p3.v_electrical_field(
        prueba) + p4.v_electrical_field(prueba)
    print(f'----- el campo electrico generado por las cargas es de {campo_electrico}')

def punto4():
    # particula de prueba
    prueba = Custom3DVector(0.5, 1.5)

    # cargas
    cargas = []
    num_cargas = 5000
    ratio = 3.0  # radio de la esfera
    for e in range(1, num_cargas):
        x = round(random.uniform(-3.0, 3.0), 2)
        y = round(random.uniform(-3.0, 3.0), 2)
        try:
            z = math.sqrt((ratio ** 2) - (x ** 2) - (y ** 2))
        except ValueError:
            continue
        r1 = Custom3DVector(x, y, z)
        p1 = ElectricParticle(-1.602e-19, r1)
        cargas.append(p1)

        r2 = Custom3DVector(x, y, -1 * z)
        p2 = ElectricParticle(-1.602e-19, r2)
        cargas.append(p2)

    campos = [c.s_electrical_field(prueba.norm()) for c in cargas]

    draw_electrical_particles_in_sphere(cargas, ratio)

    print(f'----- el campo electrico generado por las {num_cargas} cargas es de {sum(campos)}')

if __name__ == '__main__':
    print('running ...')

    punto4()

    print('closing ...')


import csv
from datetime import datetime
from flask import g
import requests
from xml.etree import ElementTree
from .database import Database

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        g._database = Database()
    return g._database

def importer_piscines():
    url = "https://data.montreal.ca/dataset/4604afb7-a7c4-4626-a3ca-e136158133f2/resource/cbdca706-569e-4b4a-805d-9af73af03b14/download/piscines.csv"
    reponse = requests.get(url)
    if reponse.status_code == 200:
        decoded_content = reponse.content.decode('utf-8')
        cr = csv.reader(decoded_content.splitlines(), delimiter=',')
        piscines = list(cr)
        premiere_ligne = True
        for row in piscines:
            if premiere_ligne:
                premiere_ligne = False
            else:
                get_db().inserer_piscine(row[0], row[1], row[2],
                                         uniformiser_arrondissement(row[3]),
                                         row[4], row[5], row[6], row[7],
                                         row[8], row[9], row[10], row[11])

def importer_patinoires():
    url = "https://data.montreal.ca/dataset/225ac315-49fe-476f-95bd-a1ce1648a98c/resource/5d1859cc-2060-4def-903f-db24408bacd0/download/l29-patinoire.xml"
    reponse = requests.get(url)
    if reponse.status_code == 200:
        racine = ElementTree.fromstring(reponse.content)

        for patinoire in racine:
            arrondissement = patinoire[0].text.strip()
            nom = patinoire[1][0].text.strip()
            derniere_date = datetime.fromisoformat("2000-01-01 00:00:00")
            for condition in patinoire[1].iter('condition'):
                if datetime.fromisoformat(
                        condition[0].text.strip()) > derniere_date:

                    date = condition[0].text.strip()
                    ouvert = condition[1].text.strip()
                    deblaye = condition[2].text.strip()
                    arrose = condition[3].text.strip()
                    resurface = condition[4].text.strip()
                    derniere_date = datetime.fromisoformat(
                        condition[0].text.strip())
            get_db().inserer_patinoire(arrondissement, nom, date, ouvert,
                                       deblaye, arrose, resurface)

def importer_glissades():
    url = "http://www2.ville.montreal.qc.ca/services_citoyens/pdf_transfert/L29_GLISSADE.xml"
    reponse = requests.get(url)
    if reponse.status_code == 200:
        racine = ElementTree.fromstring(reponse.content)

        for glissade in racine.findall('glissade'):
            nom = glissade.find('nom').text
            nom_arrondissement = glissade.find('arrondissement')
            arrondissement = nom_arrondissement.find('nom_arr').text
            cle = nom_arrondissement.find('cle').text
            date_maj = nom_arrondissement.find('date_maj').text
            ouvert = glissade.find('ouvert').text
            deblaye = glissade.find('deblaye').text
            condition = glissade.find('condition').text
            get_db().inserer_glissade(nom, arrondissement, cle,
                                      date_maj, ouvert, deblaye, condition)

def importer_donnees():
    importer_piscines()
    importer_patinoires()
    importer_glissades()

def uniformiser_arrondissement(arrondissement):
    if "Ahuntsic" in arrondissement:
        return "Ahuntsic - Cartierville"
    elif "Côte-des-Neiges" in arrondissement:
        return "Côte-des-Neiges - Notre-Dame-de-Grâce"
    elif "L'Île-Bizard" in arrondissement:
        return "L'Île-Bizard - Sainte-Geneviève"
    elif "Mercier" in arrondissement:
        return "Mercier - Hochelaga-Maisonneuve"
    elif "Pierrefonds" in arrondissement:
        return "Pierrefonds - Roxboro"
    elif "Rivière-des-Prairies" in arrondissement:
        return "Rivière-des-Prairies - Pointe-aux-Trembles"
    elif "Rosemont" in arrondissement:
        return "Rosemont - La Petite-Patrie"
    elif "Villeray" in arrondissement:
        return "Villeray-Saint-Michel - Parc-Extension"
    else:
        return arrondissement


from __future__ import annotations

import pendulum
import pytz
from datetime import datetime, time
from airflow import DAG
from airflow.models.baseoperator import chain
from airflow.operators.empty import EmptyOperator
from airflow.operators.python import ShortCircuitOperator
from airflow.utils.trigger_rule import TriggerRule

with DAG(
    dag_id="example_short_circuit_operator",
    start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
    catchup=False,
    tags=["example"],
) as dag:

    def check_if_day():
        tz = pytz.timezone("Europe/Berlin")
        now = datetime.now(tz)
        print(now)
        if time(7) < now.time() <= time(19):
            return True
        else:
            return False

    def check_if_nght():
        tz = pytz.timezone("Europe/Berlin")
        now = datetime.now(tz)
        print(now)
        if time(19) < now.time() <= time(24) or time(0) <= now.time() <= time(7):
            return True
        else:
            return False

    cond_true_day = ShortCircuitOperator(
        task_id="cond_true_day",
        ignore_downstream_trigger_rules=False,
        python_callable=check_if_day,
    )

    cond_true_night = ShortCircuitOperator(
        task_id="cond_true_night",
        ignore_downstream_trigger_rules=False,
        python_callable=check_if_nght,
    )

    [task_1, task_2, task_3, task_4, task_5] = [
        EmptyOperator(task_id=f"task_{i}") for i in range(1, 6)
    ]

    task_6 = EmptyOperator(task_id="task_6", trigger_rule=TriggerRule.ALL_DONE)

    chain(
        task_1,
        [cond_true_day, cond_true_night],
        [task_2, task_3],
        [task_4, task_5],
        task_6,
    )


import sys
import pyperclip
import pickle

def yn_input(prompt):
    """get yes/no type input from user

    Args:
        prompt (str): the question to ask user

    Returns:
        bool: True if input starts with y, False if input starts with n
    """
    raw = input(prompt+"(yes/no): ")
    acceptable_input = ['y','ye','yes','yeah','n','no','nah','naw', '']
    if raw not in acceptable_input:
        print('I am not sure what you typed in, please try again.')
        return yn_input(prompt)
    if len(raw) > 0 and raw[0].lower() == 'y':
        return True
    return False

def str_input():
    """get string input from user (multi lines or single line)

    Returns:
        str: user input
    """
    if yn_input("Will you be inputting multiple lines?"):
        print("What is the value? (press control-z and enter to end)")
        return "".join(sys.stdin.readlines())
    else:
        return input("What is the value? (press enter to end):")

def print_entry(key, val):
    """print one entry in the database, hide part of the value if it's too long

    Args:
        key (str): the key to print
        val (str): the value to print
    """
    print(key + " : " + val.replace("\n", " ")[0:10], end="")
    if len(val) >= 10:
        print("...", end="")
    print()

def get(key):
    """get the value of given key from database and copy to clipboard

    Args:
        key (str): the key to get the value from
    """
    key = key.strip()
    if key in clipboard:
        result = clipboard[key]
        print_entry(key, result)
        pyperclip.copy(result)
        print('Value of \"' + key + '\" copied to clipboard.')
    else:
        print('There is no value for \"' + key+'\"')

def edit():
    """edit specific entry in database
    """
    print_keys()
    key = input("What is the key that you want to edit?")
    if(key in clipboard):
        clipboard[key] = str_input()
    else:
        if yn_input("Key \"" + key + "\" cannot be found, wanna add it?"):
            clipboard[key] = str_input()

def add():
    """ask the user for input and add an entry to the database
    """
    newKey = input("What is the key that you want to add?")
    if newKey in clipboard.keys():
        if(not yn_input("There is already an entry, want to edit it?")):
            return
    clipboard[newKey] = str_input()

def delete():
    """delete an entry from the database
    """
    print_keys()
    key = input("What is the key that you want to delete?")
    if key in clipboard.keys():
        print("Key \""+key+"\" deleted")
        clipboard.pop(key)
    else:
        print("Key \"" + key + "\" cannot be found.")

def print_keys():
    """print all existing keys in the database
    """
    print("The keys that you saved are: ")
    for keyword in clipboard.keys():
        print(keyword, end=', ')
    print()

def print_all():
    """print all entries in the database (key:value)
    """
    print("\nYour Clipboard:")
    for keyword in clipboard.keys():
        print_entry(keyword, clipboard.get(keyword))
    print()

def read():
    try:
        file = open('mclip.dat', 'rb')
        data = pickle.load(file)
        file.close()
        return data
    except FileNotFoundError:
        print("File \"mclip.dat\" is not found and will be created")
        return {}  # create a new dictionary clipboard if the file does not exist

def write():
    file = open("mclip.dat", "wb")
    pickle.dump(clipboard, file)
    file.close()

def main(command):
    if command == 'edit':
        edit()
    elif command == 'add':
        add()
    elif command == 'delete':
        delete()
    elif command == 'all':
        print_all()
    elif command == 'keys':
        print_keys()
    else:
        get(command)
    write()
    if(yn_input("Anything else")):
        print()
        print_keys()
        command = input('What else do you want to do: ')
        main(command)

if __name__ == '__main__':
    clipboard = read()
    # when the user types "mclip" and no argument
    if len(sys.argv) < 2:
        print('\nUsage: py mclip.py [command] - key/add/edit/delete/keys/all/exit')
        sys.argv.append(input("What do you want to do?").lower())
        if sys.argv[1] == 'exit':
            sys.exit()
    main(sys.argv[1].lower())