import pandas as pd
from unittest.mock import patch
from src.transformation import (format_dim_staff,
                                format_dim_location,
                                format_dim_design,
                                format_dim_date,
                                format_dim_currency,
                                format_dim_counterparty,
                                format_dim_transaction,
                                format_dim_payment_type,
                                format_fact_sales_order,
                                format_fact_purchase_order,
                                format_fact_payment,
                                transform_data)
import datetime
import logging

logger = logging.getLogger("testing_transform")
logger.setLevel(logging.INFO)

test_datetime = datetime.datetime.fromisoformat("2000-01-01T14:20:51.563000")
test_date = datetime.date.fromisoformat("2000-01-01")
test_td = datetime.timedelta(days=1)
test_time = datetime.time.fromisoformat("14:20:51.563000")

def test_format_dim_staff_returns_expected_output():
    staff_data = {
        "staff_id": [1, 2, 3],
        "first_name": ["Sam", "Joe", "Max"],
        "last_name": ["Ant", "Boo", "Car"],
        "department_id": [1, 2, 3],
        "email_address": ["a@b.com", "b@c.com", "c@d.com"],
        "created_at": [test_datetime, test_datetime, test_datetime],
        "last_updated": [test_datetime, test_datetime, test_datetime]
    }
    staff_df = pd.DataFrame(data=staff_data)
    dept_data = {
        "department_id": [1, 2, 3],
        "department_name": ["a", "b", "c"],
        "location": ["loca", "locb", "locc"],
        "manager": ["mrs a", "mr b", "ms c"],
        "created_at": [test_datetime, test_datetime, test_datetime],
        "last_updated": [test_datetime, test_datetime, test_datetime]
    }
    dept_df = pd.DataFrame(data=dept_data)

    test_dim_staff_data = {
        "staff_id": [1, 2, 3],
        "first_name": ["Sam", "Joe", "Max"],
        "last_name": ["Ant", "Boo", "Car"],
        "department_name": ["a", "b", "c"],
        "location": ["loca", "locb", "locc"],
        "email_address": ["a@b.com", "b@c.com", "c@d.com"]
    }
    dim_staff_df = pd.DataFrame(data=test_dim_staff_data)
    assert format_dim_staff(staff_df, dept_df).equals(dim_staff_df)

def test_format_dim_location_returns_expected_output():
    location_data = {
        "address_id": [1, 2, 3],
        "address_line_1": ["17", "23", "44"],
        "address_line_2": ["Green Street", "Yellow Road", None],
        "district": ["districta", "districtb", "districtc"],
        "city": ["citya", "cityb", "cityc"],
        "postal_code": ["123", "ABC", "987"],
        "country": ["Uk", "USA", "Ireland"],
        "phone": [123, 456, 789],
        "created_at": [test_datetime, test_datetime, test_datetime],
        "last_updated": [test_datetime, test_datetime, test_datetime]
    }
    location_df = pd.DataFrame(data=location_data)

    test_dim_location_data = {
        "location_id": [1, 2, 3],
        "address_line_1": ["17", "23", "44"],
        "address_line_2": ["Green Street", "Yellow Road", None],
        "district": ["districta", "districtb", "districtc"],
        "city": ["citya", "cityb", "cityc"],
        "postal_code": ["123", "ABC", "987"],
        "country": ["Uk", "USA", "Ireland"],
        "phone": [123, 456, 789]
    }
    dim_location_df = pd.DataFrame(data=test_dim_location_data)
    assert format_dim_location(location_df).equals(dim_location_df)

def test_format_dim_design_returns_expected_output():
    design_data = {
        "design_id": [1, 2, 3],
        "created_at": [test_datetime, test_datetime, test_datetime],
        "design_name": ["design_c", "design_b", "design_a"],
        "file_location": ["loc_a", "loc_b", "loc/c"],
        "file_name": ["filename_a", "filename_b", "filename_c"],
        "last_updated": [test_datetime, test_datetime, test_datetime]
    }
    design_df = pd.DataFrame(data=design_data)

    test_dim_design_data = {
        "design_id": [1, 2, 3],
        "design_name": ["design_c", "design_b", "design_a"],
        "file_location": ["loc_a", "loc_b", "loc/c"],
        "file_name": ["filename_a", "filename_b", "filename_c"]
    }
    dim_design_df = pd.DataFrame(data=test_dim_design_data)
    assert format_dim_design(design_df).equals(dim_design_df)

def test_format_dim_transaction_returns_expected_output():
    transaction_data = {
        "transaction_id": [1, 2, 3],
        "transaction_type": ["type1", "type2", "type3"],
        "sales_order_id": [1, 2, 3],
        "purchase_order_id": [1, 2, 3]
    }
    transaction_df = pd.DataFrame(data=transaction_data)

    test_dim_transaction_data = {
        "transaction_id": [1, 2, 3],
        "transaction_type": ["type1", "type2", "type3"],
        "sales_order_id": [1, 2, 3],
        "purchase_order_id": [1, 2, 3]
    }
    dim_transaction_df = pd.DataFrame(data=test_dim_transaction_data)
    dim_transaction_df = dim_transaction_df.astype(
        {'sales_order_id': 'Int64', 'purchase_order_id': 'Int64'})
    assert format_dim_transaction(transaction_df).equals(dim_transaction_df)

def test_format_dim_payment_type_returns_expected_output():
    payment_type_data = {
        "payment_type_id": [1, 2, 3],
        "payment_type_name": ["name1", "name2", "name3"]
    }
    payment_type_df = pd.DataFrame(data=payment_type_data)

    test_dim_payment_type_data = {
        "payment_type_id": [1, 2, 3],
        "payment_type_name": ["name1", "name2", "name3"]
    }
    dim_payment_type_df = pd.DataFrame(data=test_dim_payment_type_data)
    assert format_dim_payment_type(payment_type_df).equals(dim_payment_type_df)

def test_format_dim_date_returns_expected_output():
    start = '2000-01-01'
    end = '2000-01-10'

    test_date = datetime.datetime.fromisoformat("2000-01-01")

    test_dim_date_data = {
        "date_id": [test_date, test_date + test_td, test_date + (test_td * 2),
                    test_date + (test_td * 3), test_date + (test_td * 4),
                    test_date + (test_td * 5), test_date + (test_td * 6),
                    test_date + (test_td * 7), test_date + (test_td * 8),
                    test_date + (test_td * 9)],
        "year": [2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000],
        "month": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        "day": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
        "day_of_week": [5, 6, 0, 1, 2, 3, 4, 5, 6, 0],
        "day_name": ['Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday',
                     'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday'],
        "month_name": ['January', 'January', 'January', 'January', 'January',
                       'January', 'January', 'January', 'January', 'January'],
        "quarter": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    }

    dim_date_df = pd.DataFrame(data=test_dim_date_data)
    assert format_dim_date(start, end).equals(dim_date_df)

def test_format_dim_currency_returns_expected_output():
    currency_data = {
        'currency_id': [1, 2, 3],
        'currency_code': ['GBP', 'USD', 'EUR'],
        "created_at": [test_datetime, test_datetime, test_datetime],
        "last_updated": [test_datetime, test_datetime, test_datetime]
    }
    currency_df = pd.DataFrame(data=currency_data)

    test_dim_currency_data = {
        'currency_id': [1, 2, 3],
        'currency_code': ['GBP', 'USD', 'EUR'],
        'currency_name': ['British Pound sterling',
                          'United States dollar',
                          'Euro']
    }
    dim_currency_df = pd.DataFrame(data=test_dim_currency_data)

    assert format_dim_currency(currency_df).equals(dim_currency_df)

def test_format_dim_counterparty_returns_expected_output():
    counterparty_data = {
        'counterparty_id': [1, 2, 3],
        'counterparty_legal_name': ['NameA', 'NameB', 'NameC'],
        'legal_address_id': [1, 2, 3],
        'commercial_contact': ['PersonA', 'PersonB', 'PersonC'],
        'delivery_contact': ['PersonD', 'PersonE', 'PersonF'],
        'created_at': [
            test_datetime,
            test_datetime,
            test_datetime],
        'last_updated': [
            test_datetime,
            test_datetime,
            test_datetime]
    }
    counterparty_df = pd.DataFrame(data=counterparty_data)

    address_data = {
        "address_id": [1, 2, 3],
        "address_line_1": ["17", "23", "44"],
        "address_line_2": ["Green Street", "Yellow Road", None],
        "district": ["DistrictA", "DistrictB", "DistrictC"],
        "city": ["CityA", "CityB", "CityC"],
        "postal_code": ["123", "ABC", "987"],
        "country": ["UK", "USA", "Ireland"],
        "phone": [123, 456, 789],
        "created_at": [
            test_datetime,
            test_datetime,
            test_datetime],
        "last_updated": [
            test_datetime,
            test_datetime,
            test_datetime]
    }
    address_df = pd.DataFrame(data=address_data)

    test_dim_counterparty_data = {
        'counterparty_id': [1, 2, 3],
        'counterparty_legal_name': ['NameA', 'NameB', 'NameC'],
        'counterparty_legal_address_line_1': ["17", "23", "44"],
        'counterparty_legal_address_line_2': ["Green Street",
                                              "Yellow Road",
                                              None],
        'counterparty_legal_district': ["DistrictA",
                                        "DistrictB",
                                        "DistrictC"],
        'counterparty_legal_city': ["CityA",
                                    "CityB",
                                    "CityC"],
        'counterparty_legal_postal_code': ["123", "ABC", "987"],
        'counterparty_legal_country': ["UK", "USA", "Ireland"],
        'counterparty_legal_phone_number': [123, 456, 789]
    }
    dim_counterparty_df = pd.DataFrame(data=test_dim_counterparty_data)

    assert format_dim_counterparty(
        counterparty_df, address_df).equals(dim_counterparty_df)

def test_format_fact_sales_order_returns_expected_output():
    sales_order_data = {
        "sales_order_id": [1, 2, 3],
        "created_at": [test_datetime, test_datetime, test_datetime],
        "last_updated": [test_datetime, test_datetime, test_datetime],
        "design_id": ["des1", "des2", "des3"],
        "staff_id": [1, 2, 3],
        "counterparty_id": [1, 2, 3],
        "units_sold": [100, 200, 300],
        "unit_price": [2.45, 3.67, 9.87],
        "currency_id": [1, 2, 3],
        "agreed_payment_date": [test_date, test_date, test_date],
        "agreed_delivery_date": [test_date, test_date, test_date],
        "agreed_delivery_location_id": [1, 2, 3]
    }
    sales_order_df = pd.DataFrame(data=sales_order_data)
    test_fact_sales_order_data = {
        "sales_record_id": [1, 2, 3],
        "sales_order_id": [1, 2, 3],
        "created_date": [test_date, test_date, test_date],
        "created_time": [test_time, test_time, test_time],
        "last_updated_date": [test_date, test_date, test_date],
        "last_updated_time": [test_time, test_time, test_time],
        "sales_staff_id": [1, 2, 3],
        "counterparty_id": [1, 2, 3],
        "units_sold": [100, 200, 300],
        "unit_price": [2.45, 3.67, 9.87],
        "currency_id": [1, 2, 3],
        "design_id": ["des1", "des2", "des3"],
        "agreed_payment_date": [test_date, test_date, test_date],
        "agreed_delivery_date": [test_date, test_date, test_date],
        "agreed_delivery_location_id": [1, 2, 3]
    }
    fact_sales_order_df = pd.DataFrame(data=test_fact_sales_order_data)
    assert format_fact_sales_order(sales_order_df).equals(fact_sales_order_df)

def test_format_fact_purchase_order_returns_expected_output():
    purchase_order_data = {
        "purchase_order_id": [1, 2, 3],
        "created_at": [test_datetime, test_datetime, test_datetime],
        "last_updated": [test_datetime, test_datetime, test_datetime],
        "staff_id": [1, 2, 3],
        "counterparty_id": [1, 2, 3],
        "item_code": ["dummy data 1", "dummy data 2", "dummy data 3"],
        "design_id": ["des1", "des2", "des3"],
        "item_quantity": [3, 4, 9],
        "item_unit_price": [4, 8, 3],
        "currency_id": [1, 2, 3],
        "agreed_delivery_date": [test_date, test_date, test_date],
        "agreed_payment_date": [test_date, test_date, test_date],
        "agreed_delivery_location_id": [1, 2, 3]
    }
    purchase_order_df = pd.DataFrame(data=purchase_order_data)
    test_fact_purchase_order_data = {
        "purchase_record_id": [1, 2, 3],
        "purchase_order_id": [1, 2, 3],
        "created_date": [test_date, test_date, test_date],
        "created_time": [test_time, test_time, test_time],
        "last_updated_date": [test_date, test_date, test_date],
        "last_updated_time": [test_time, test_time, test_time],
        "staff_id": [1, 2, 3],
        "counterparty_id": [1, 2, 3],
        "item_code": ["dummy data 1", "dummy data 2", "dummy data 3"],
        "item_quantity": [3, 4, 9],
        "item_unit_price": [4, 8, 3],
        "currency_id": [1, 2, 3],
        "agreed_delivery_date": [test_date, test_date, test_date],
        "agreed_payment_date": [test_date, test_date, test_date],
        "agreed_delivery_location_id": [1, 2, 3]
    }
    fact_purchase_order_df = pd.DataFrame(data=test_fact_purchase_order_data)
    assert format_fact_purchase_order(purchase_order_df).equals(
        fact_purchase_order_df)

def test_format_fact_payment_returns_expected_output():
    payment_data = {
        "payment_id": [1, 2, 3],
        "created_at": [test_datetime, test_datetime, test_datetime],
        "last_updated": [test_datetime, test_datetime, test_datetime],
        "transaction_id": [1, 2, 3],
        "counterparty_id": [1, 2, 3],
        "payment_amount": [3, 7, 9],
        "currency_id": [1, 2, 3],
        "payment_type_id": ["type1", "type2", "type3"],
        "paid": [True, False, True],
        "payment_date": [test_date, test_date, test_date],
        "company_ac_number": [1, 2, 3],
        "counterparty_ac_number": [1, 2, 3]
    }
    payment_df = pd.DataFrame(data=payment_data)
    test_fact_payment_data = {
        "payment_record_id": [1, 2, 3],
        "payment_id": [1, 2, 3],
        "created_date": [test_date, test_date, test_date],
        "created_time": [test_time, test_time, test_time],
        "last_updated_date": [test_date, test_date, test_date],
        "last_updated_time": [test_time, test_time, test_time],
        "transaction_id": [1, 2, 3],
        "counterparty_id": [1, 2, 3],
        "payment_amount": [3, 7, 9],
        "currency_id": [1, 2, 3],
        "payment_type_id": ["type1", "type2", "type3"],
        "paid": [True, False, True],
        "payment_date": [test_date, test_date, test_date]
    }
    fact_payment_df = pd.DataFrame(data=test_fact_payment_data)
    assert format_fact_payment(payment_df).equals(fact_payment_df)

@patch("src.transformation.export_parquet_to_s3")
@patch("src.transformation.load_csv_from_s3")
def test_transform_data(mock_load, mock_export):

    mock_load.side_effect = load_func
    mock_export.return_value = True

    files_dict = {}
    files_dict["dim_staff.parquet"] = True
    files_dict["dim_location.parquet"] = True
    files_dict["dim_design.parquet"] = True
    files_dict["dim_date.parquet"] = True
    files_dict["dim_currency.parquet"] = True
    files_dict["dim_counterparty.parquet"] = True
    files_dict["dim_transaction.parquet"] = True
    files_dict["dim_payment_type.parquet"] = True
    files_dict["fact_sales_order.parquet"] = True
    files_dict["fact_purchase_order.parquet"] = True
    files_dict["fact_payment.parquet"] = True
    assert transform_data({}, {}) == files_dict

@patch("src.transformation.export_parquet_to_s3")
@patch("src.transformation.load_csv_from_s3")
def test_transform_data_load_errors(mock_load, mock_export, caplog):
    mock_load.side_effect = FileNotFoundError("Bucket not found.")
    mock_export.return_value = True
    transform_data({}, {})
    assert caplog.records[0].levelno == logging.ERROR
    assert caplog.records[0].msg == "Error when retrieving files: "\
        "Bucket not found."

@patch("src.transformation.export_parquet_to_s3")
@patch("src.transformation.load_csv_from_s3")
def test_transform_data_formatting_errors(mock_load, mock_export, caplog):
    mock_load.side_effect = load_func_error
    mock_export.return_value = True
    transform_data({}, {})
    assert caplog.records[0].levelno == logging.ERROR
    assert caplog.records[0].msg == "Error when formatting files: "\
        "Can only merge Series or DataFrame objects,"\
        " a <class 'bool'> was passed"

@patch("src.transformation.export_parquet_to_s3")
@patch("src.transformation.load_csv_from_s3")
def test_transform_data_exporting_errors(mock_load, mock_export, caplog):
    mock_load.side_effect = load_func
    mock_export.side_effect = AttributeError(
        "Object passed to the function is not of type DataFrame.")
    transform_data({}, {})
    assert caplog.records[0].levelno == logging.ERROR
    assert caplog.records[0].msg == "Error when uploading files: "\
        "Object passed to the function is not of type DataFrame."

def load_func(bucket, file, parse_dates=[]):
    if file == "staff.csv":
        staff_data = {
            "staff_id": [1, 2, 3],
            "first_name": ["Sam", "Joe", "Max"],
            "last_name": ["Ant", "Boo", "Car"],
            "department_id": [1, 2, 3],
            "email_address": ["a@b.com", "b@c.com", "c@d.com"],
            "created_at": [test_datetime, test_datetime, test_datetime],
            "last_updated": [test_datetime, test_datetime, test_datetime]
        }
        return pd.DataFrame(data=staff_data)
    elif file == "department.csv":
        dept_data = {
            "department_id": [1, 2, 3],
            "department_name": ["a", "b", "c"],
            "location": ["loca", "locb", "locc"],
            "manager": ["mrs a", "mr b", "ms c"],
            "created_at": [test_datetime, test_datetime, test_datetime],
            "last_updated": [test_datetime, test_datetime, test_datetime]
        }
        return pd.DataFrame(data=dept_data)
    elif file == "counterparty.csv":
        counterparty_data = {
            'counterparty_id': [1, 2, 3],
            'counterparty_legal_name': ['NameA', 'NameB', 'NameC'],
            'legal_address_id': [1, 2, 3],
            'commercial_contact': ['PersonA', 'PersonB', 'PersonC'],
            'delivery_contact': ['PersonD', 'PersonE', 'PersonF'],
            'created_at': [
                test_datetime,
                test_datetime,
                test_datetime],
            'last_updated': [
                test_datetime,
                test_datetime,
                test_datetime]
        }
        return pd.DataFrame(data=counterparty_data)
    elif file == "transaction.csv":
        transaction_data = {
            "transaction_id": [1, 2, 3],
            "transaction_type": ["type1", "type2", "type3"],
            "sales_order_id": [1, 2, 3],
            "purchase_order_id": [1, 2, 3]
        }
        return pd.DataFrame(data=transaction_data)
    elif file == "payment_type.csv":
        payment_type_data = {
            "payment_type_id": [1, 2, 3],
            "payment_type_name": ["name1", "name2", "name3"]
        }
        return pd.DataFrame(data=payment_type_data)
    elif file == "currency.csv":
        currency_data = {
            'currency_id': [1, 2, 3],
            'currency_code': ['GBP', 'USD', 'EUR'],
            "created_at": [test_datetime, test_datetime, test_datetime],
            "last_updated": [test_datetime, test_datetime, test_datetime]
        }
        return pd.DataFrame(data=currency_data)
    elif file == "address.csv":
        location_data = {
            "address_id": [1, 2, 3],
            "address_line_1": ["17", "23", "44"],
            "address_line_2": ["Green Street", "Yellow Road", None],
            "district": ["districta", "districtb", "districtc"],
            "city": ["citya", "cityb", "cityc"],
            "postal_code": ["123", "ABC", "987"],
            "country": ["Uk", "USA", "Ireland"],
            "phone": [123, 456, 789],
            "created_at": [test_datetime, test_datetime, test_datetime],
            "last_updated": [test_datetime, test_datetime, test_datetime]
        }
        return pd.DataFrame(data=location_data)
    elif file == "design.csv":
        design_data = {
            "design_id": [1, 2, 3],
            "created_at": [test_datetime, test_datetime, test_datetime],
            "design_name": ["design_c", "design_b", "design_a"],
            "file_location": ["loc_a", "loc_b", "loc/c"],
            "file_name": ["filename_a", "filename_b", "filename_c"],
            "last_updated": [test_datetime, test_datetime, test_datetime]
        }
        return pd.DataFrame(data=design_data)
    elif file == "sales_order.csv":
        sales_order_data = {
            "sales_order_id": [1, 2, 3],
            "created_at": [test_datetime, test_datetime, test_datetime],
            "last_updated": [test_datetime, test_datetime, test_datetime],
            "design_id": ["des1", "des2", "des3"],
            "staff_id": [1, 2, 3],
            "counterparty_id": [1, 2, 3],
            "units_sold": [100, 200, 300],
            "unit_price": [2.45, 3.67, 9.87],
            "currency_id": [1, 2, 3],
            "agreed_delivery_date": [test_date, test_date, test_date],
            "agreed_payment_date": [test_date, test_date, test_date],
            "agreed_delivery_location_id": [1, 2, 3]
        }
        return pd.DataFrame(data=sales_order_data)
    elif file == "payment.csv":
        payment_data = {
            "payment_id": [1, 2, 3],
            "created_at": [test_datetime, test_datetime, test_datetime],
            "last_updated": [test_datetime, test_datetime, test_datetime],
            "transaction_id": [1, 2, 3],
            "counterparty_id": [1, 2, 3],
            "payment_amount": [3, 7, 9],
            "currency_id": [1, 2, 3],
            "payment_type_id": ["type1", "type2", "type3"],
            "paid": [True, False, True],
            "payment_date": [test_date, test_date, test_date],
            "company_ac_number": [1, 2, 3],
            "counterparty_ac_number": [1, 2, 3]
        }
        return pd.DataFrame(data=payment_data)
    elif file == "purchase_order.csv":
        purchase_order_data = {
            "purchase_order_id": [1, 2, 3],
            "created_at": [test_datetime, test_datetime, test_datetime],
            "last_updated": [test_datetime, test_datetime, test_datetime],
            "staff_id": [1, 2, 3],
            "counterparty_id": [1, 2, 3],
            "item_code": ["dummy data 1", "dummy data 2", "dummy data 3"],
            "design_id": ["des1", "des2", "des3"],
            "item_quantity": [3, 4, 9],
            "item_unit_price": [4, 8, 3],
            "currency_id": [1, 2, 3],
            "agreed_delivery_date": [test_date, test_date, test_date],
            "agreed_payment_date": [test_date, test_date, test_date],
            "agreed_delivery_location_id": [1, 2, 3]
        }
        return pd.DataFrame(data=purchase_order_data)

def load_func_error(bucket, file, parse_dates=[]):
    if file == "staff.csv":
        return True
    elif file == "department.csv":
        return True
    elif file == "counterparty.csv":
        return True
    elif file == "transaction.csv":
        return True
    elif file == "payment_type.csv":
        return True
    elif file == "currency.csv":
        return True
    elif file == "address.csv":
        return True
    elif file == "design.csv":
        return True
    elif file == "sales_order.csv":
        return True
    elif file == "purchase_order.csv":
        return True
    elif file == "payment.csv":
        return True


"""
API to interact with email services (via courier)
"""
from config import COURIER_API_TOKEN
import utils
from trycourier import Courier
from .communications_mapping import COMMS_TO_EVENT_MAP
import config

@utils.logged
class Notifier:
    def __init__(self):
        self.client = Courier(auth_token=COURIER_API_TOKEN)

    def notify(
        self, comms_name, recipient_id, data, profile=None, idempotency_key=None
    ):
        """Send communications via courier"""
        event_id = COMMS_TO_EVENT_MAP[comms_name]
        resp = self.client.send(
            event=event_id,
            recipient=recipient_id,
            data=data,
            profile=profile,
            idempotency_key=idempotency_key,
        )
        return resp

    def notify_organizer(self, message):
        """Send communications via courier"""
        event_id = COMMS_TO_EVENT_MAP["generic_organizer_notification"]
        resp = self.client.send(
            event=event_id,
            recipient="organizer",
            data={"message": message},
            profile=config.ADMIN_NOTIFICATION_PROFILE,
            idempotency_key=None,
        )
        return resp

    def replace_or_create_profile(self, recipient_id, profile):
        """Create or replace profile"""
        resp = self.client.replace_profile(recipient_id, profile)
        return resp

    def update_or_create_profile(self, recipient_id, profile):
        """Create or update profile"""
        resp = self.client.merge_profile(recipient_id, profile)
        return resp


from abc import ABC, abstractmethod
from typing import Any, Generic

import bpy
from mathutils import Vector

from ..blendutil import set_active_layer_collection
from ..datamodel import CellBlock, IVector, T_state

C = bpy.context
D = bpy.data

class CellBlockView(ABC, Generic[T_state]):
    __slots__ = (
        '_cells', '_block_name', '_cell_size', '_padding', '_collection', '_meshes')

    def __init__(self,
        cells: CellBlock[T_state],
        block_name: str,
        size: float,
        padding: float
    ):
        self._cells = cells
        self._block_name = block_name
        self._cell_size = size
        self._padding = padding
        self._meshes: dict[IVector, Any] = {}

        # Create a collection for the cell block and set it active
        self._collection = D.collections.new(block_name)
        C.scene.collection.children.link(self._collection)
        set_active_layer_collection(block_name)

        # Create the individual cells
        self.make_meshes()

    def make_meshes(self):
        """Creates a mesh for each cell in the cell block.

        Cells are added to the currently active layer collection.
        """
        sx, sy, sz = self._cells.size
        box = self._cell_size + (self._padding * 2)
        center = Vector((
            (sx * box) / 2,
            (sy * box) / 2,
            (sz * box) / 2
        ))
        offset = Vector((
            (box / 2) - center.x,
            (box / 2) - center.y,
            (box / 2) - center.z
        ))

        for xyz in self._cells:
            grid_x, grid_y, grid_z = xyz
            x = (grid_x * box) + offset.x
            y = (grid_y * box) + offset.y
            z = (grid_z * box) + offset.z
            self.add_cell_view(self._cell_size, Vector((x, y, z)))
            cube = C.object
            cube.name = self._cells.name_of(xyz)
            self._meshes[xyz] = cube

    @property
    def cells(self) -> CellBlock[T_state]:
        return self._cells

    @property
    def name(self) -> str:
        return self._block_name

    @property
    def collection(self) -> Any:
        return self._collection

    @property
    def meshes(self) -> dict[IVector, Any]:
        return self._meshes

    def update(self):
        """Update the cells to match the states of the backing cell block."""
        for (xyz, mesh) in self._meshes.items():
            state = self._cells[xyz]
            self.update_cell_view(mesh, state)

    @abstractmethod
    def add_cell_view(self, size: float, location: Vector) -> None:
        """Adds a Blender object to the active layer collection that will
        represent a grid cell.

        Args:
            size: The size of the object in Blender units.
            location: The location of the object in Blender space.
        """

    @abstractmethod
    def update_cell_view(self, cell_view: Any, state: T_state) -> None:
        """Updates the given Blender object using the given ``state``.

        Args:
            cell_view: The Blender object to adjust.
            state: The state to inform the adjustment.
        """


"""
Allows users to track their computation graph for visualization. Data is "tracked" by wrapping it in a `GraphData`
object, which acts as a passthrough wrapper that records the op that created the data and the values of the data that
should be visualized. Function calls are "tracked" by `GraphOp` objects, which record the function that was called
and the arguments it was given. Finally, containers are "tracked" with `GraphContainer` objects, which record their
contents.

See viz.md for underlying principles and concepts.
"""
import wrapt
import inspect
from collections import deque

class Nestable:
    """A parent class to `GraphOp` and `GraphContainer`, representing an object which might be nested in a hierarchy of
    containers."""
    def __init__(self):
        # Each `Nestable` may have either 0 or 1 containers, though that container might have many items and a
        # container of its own. Containers should also be `Nestable` instances.
        self.container = None

    def get_outermost_parent(self):
        """Returns the first `Nestable` with no container, found by iterating through this `Nestable`'s container
        hierarchy.

        The "outermost parent" of a `Nestable` n is the first `Nestable` with no container found in the hierarchy of
        n. This could be n itself, if n has no container.

        Returns:
            (Nestable): This instance's outermost parent.
        """
        return self if self.container is None else self.container.get_outermost_parent()

# ======================================================================================================================
# Graph operations.
# -----------------
# A `GraphOp` object records a single function call and is created whenever a function wrapped by an `OpGenerator`
# object is called.
# ======================================================================================================================

class GraphOp(Nestable):
    """A record of a single function execution."""
    def __init__(self, fn, args, kwargs):
        """Constructor.

        Args:
            fn (Callable): The function executed in the op.
            args (tuple): A sequence of positional arguments that were passed to the function at execution.
            kwargs (dict): A dictionary of keyword arguments passed to the function at execution.
        """
        super(GraphOp, self).__init__()
        self.fn = fn

        try:
            self.fn_name = fn.__name__
        except AttributeError:
            self.fn_name = fn.__class__.__name__

        # built-in functions don't have signatures, so we make them up
        try:
            if hasattr(fn, 'forward'):
                # `getfullargspec` will work on PyTorch modules, but won't get arg names. Need to get from `forward`
                # directly.
                fn_spec = inspect.getfullargspec(fn.forward)
            else:
                fn_spec = inspect.getfullargspec(fn)
            arg_names = fn_spec.args
            varargs = fn_spec.varargs
        except TypeError:
            arg_names = ['{}[{}]'.format(self.fn_name, i) for i in range(len(args))]
            varargs = 'args'

        arg_names = arg_names[(len(arg_names) - len(args)):] + [varargs for _ in range(len(args) - len(arg_names))]

        # Pytorch modules don't have a `__name__` field
        try:
            self.name = self.fn.__name__
        except AttributeError:
            self.name = self.fn.__class__.__name__

        # Arguments which were tracked via a call to `track_data()` (that is, should be shown in the graph) have a
        # `GraphData` object associated with them. We only record these objects (if they exist) in `self.args`,
        # as these contain all of the information needed to visualize the data in the client. Untracked objects are
        # represented by `None` to maintain position.
        self.args = self._make_arg_list(args, arg_names)

        # Only k-v pairs where the value is "tracked" are saved in `self.kwargs`.
        kwarg_keys = list(kwargs.keys())
        self.kwargs = self._make_arg_list([kwargs[k] for k in kwarg_keys], kwarg_keys)

        # A `GraphOp` object always has temporal level 0, so any new temporal container will be able to encapsulate
        # it (unless it is already in a temporal container of the same level). The concept is explained more in the
        # Containers section.
        self.temporal_level = 0

        self.outputs = []

    def _make_arg_list(self, args, arg_names):
        # len(args) == len(arg_names)
        arg_list = []
        for i, arg in enumerate(args):
            if has_graphdata(arg):
                arg_list.append([arg_names[i], get_graphdata(arg)])
            elif hasattr(arg, '__iter__'):
                arg_list.append([arg_names[i], [get_graphdata(obj) for obj in arg if has_graphdata(obj)]])
            else:
                arg_list.append([arg_names[i]])
        return arg_list

    def get_tracked_args(self):
        """Return a list of all recorded positional and keyword arguments that are wrapped in `GraphData`.

        This is used to build the DAG, collecting all `GraphData` inputs so that their ancestors can be iterated over
        and added to the graph.

        Returns:
            (list): All positional and keyword argument values that are tracked in `GraphData`.
        """
        tracked_args = []
        for arg_list in [self.args, self.kwargs]:
            tracked_args.extend([arg[1] for arg in arg_list if len(arg) > 1 and not isinstance(arg[1], list)])
            for arg in arg_list:
                if len(arg) > 1 and isinstance(arg[1], list):
                    tracked_args.extend([arg_item for arg_item in arg[1] if arg_item is not None])
        return tracked_args

# ======================================================================================================================
# Graph data.
# -----------
# A `GraphData` object records a single immutable function input or output in the computation graph. `GraphData` can
# be used as if they were their wrapped object.
# ======================================================================================================================

class GraphData:
    """A record of a tracked function input or output."""
    def __init__(self, obj, props_to_surface=None, creator_op=None, creator_pos=-1):
        """Constructor. Stores the properties of the tracked object which should be visualized in the graph.

        `GraphData` is a "dumb" container, simply recording internally an object which was input or output to a
        `GraphOp` or tracked via `track_data()`. `obj` should be immutable; if it changes, then the graph will be
        rendered incorrectly.

        Args:
            obj (object): The immutable object wrapped by the `GraphData`.
            props_to_surface (dict or None): A dict whose keys are user-selected names and whose values are the names
                of attributes of the wrapped Python object. The value of the named attribute will be shown under the
                user-selected name when the `GraphData` is inspected in the client. See
                `GraphData.surface_prop_in_client()` for more info and `OpGenerator.__call__()` for an example of usage.
            creator_op (GraphOp): The `GraphOp` which created the wrapped object.
            creator_pos (int): The position of the object in the `creator_op`'s output tuple.
        """
        self.obj = obj
        self.creator_op = creator_op
        self.creator_pos = creator_pos
        self.props_to_surface = dict()
        if props_to_surface is not None:
            for name_in_client, attr_name in props_to_surface.items():
                self.surface_prop_in_client(name_in_client, attr_name)

    def surface_prop_in_client(self, name_in_client, attr_name):
        """Add a new property to the `GraphData`'s visualization in the client.

        The client by default does not show all attributes of the wrapped object; this is helpful for objects like
        `torch.Variable`, which have many unnecessary fields. These fields also do not have names that are useful for
        the client to see. Instead, the `GraphData` object maintains a dictionary mapping descriptive property names
        to names of the wrapped object's attributes. When the `GraphData` is visualized, only these attribute values
        are shown, and under the user-selected name.

        Args:
            name_in_client (str): The property name, as will be shown in the client.
            attr_name (str or None): The name of the wrapped Python object's attribute, whose value will be shown
                associated with `name_in_client`. A value of `None` will associate the wrapped object itself with
                `name_in_client`, rather than a single one of its attributes.
        """
        self.props_to_surface[name_in_client] = attr_name

    def get_visualization_dict(self):
        """Returns a mapping of user-selected names to attributes of the wrapped Python object.

        The dict values will not be string names of the attributes, but rather the values of the attributes themselves.

        Returns:
            (dict): A dict of the form {user-selected name: value to visualize}.
        """
        return {
            key: getattr(self.obj, attr) if attr is not None else self.obj
            for key, attr in self.props_to_surface.items()
        }

# ======================================================================================================================
# Graph containers.
# -----------------
# A `GraphContainer` groups together `GraphOp` and other `GraphContainer` objects to provide abstraction in
# visualization. Containers are either abstractive, which collapses a subnetwork into a "black box" function,
# or temporal, which conceals everything outside of it, showing only the single "flow" it contains. See viz.md for
# information about the difference between the two, as well as the concept of "temporal subdivision" and "temporal
# level".
# ======================================================================================================================

# TODO handle paralellism
class GraphContainer(Nestable):
    """Represents a collection of grouped `GraphOp` and `GraphContainer` objects."""
    def __init__(self, contents, name='null', temporal_level=0, temporal_step=-1):
        """Constructor.
        Args:
            contents (set): A list of `GraphOp` and `GraphContainer` objects grouped by the instance,
                or None if no items are grouped yet.
            temporal_level (int): The temporal "height" of the container. Abstractive containers have temporal level
                0, and temporal containers have level > 0. New temporal containers can encapsulate only containers of
                level `temporal_level-1`.
        """
        super(GraphContainer, self).__init__()
        self.contents = contents
        self.temporal_level = temporal_level
        self.height = max([getattr(c, 'height', 0) + 1 for c in self.contents])
        self.temporal_step = temporal_step
        self.fn_name = name

def _build_abstractive_container(outputs, inputs, container_name):
    """Creates an abstractive container enveloping all ops between the container's proposed outputs and inputs.

    Builds the DAG backwards from the outputs in a breadth-first search, stopping beams at any encountered
    `GraphData` which is in the set of inputs. Whenever a new `GraphOp` is found during search, its outermost parent
    is added to the new container (see the definition of "outermost parent" in `Nestable.get_outermost_parent()`. After
    search terminates, all of the new container's contents have their `container` field updated to point to the new
    container.

    Args:
        outputs (list): `GraphData` objects which serve as the outputs of the new abstractive container.
        inputs (set): `GraphData` objects which serve as the inputs of the new abstractive container.
        container_name (str): The name that should be shown when the container is collapsed in the client.
    """
    contents = set()
    ops_checked = set()
    data_to_check = deque(outputs)
    while len(data_to_check) > 0:
        data = data_to_check.popleft()
        if data in inputs:
            continue
        creator_op = data.creator_op
        if creator_op is not None and creator_op not in ops_checked:
            outermost_parent = creator_op.get_outermost_parent()
            contents.add(outermost_parent)
            data_to_check.extend(creator_op.get_tracked_args())
            ops_checked.add(creator_op)
    container = GraphContainer(contents, name=container_name)
    # Update newly contained objects after iterating through the graph to prevent the new container from containing
    # itself (consider ops op1, op2, which share container c1. We are adding a new container c2. If we update the
    # container of op1 during iteration, then c1's container becomes c2. When we check op2, its outermost container
    # is now c2, meaning c2's container would become c2).
    for item in container.contents:
        item.container = container

# ======================================================================================================================
# Public API.
# -----------------
# The canonical ways of creating `GraphData`, `GraphOp`, and `GraphContainer` instances. Instances should not be
# directly created, and should be created only using the following methods.
# ======================================================================================================================

def track_data(obj, props_to_surface, creator_op=None, creator_pos=-1):
    """Creates a `GraphData` object which records the properties of `obj` and allows it to be shown in the graph.

    Any object which is not the output of a tracked function (see `OpGenerator`) is not, by default, shown in the
    computation graph. Leaf data nodes can be added to the graph via `track_data()`.

    Args:
        obj (object): Python object to add to the graph.
        props_to_surface (dict or None): A dict whose keys are user-selected names and whose values are the names
            of attributes of the wrapped Python object. The value of the named attribute will be shown under the
            user-selected name when the `GraphData` is inspected in the client. See
            `GraphData.surface_prop_in_client()` for more info and `OpGenerator.__call__()` for an example of usage.
        creator_op (GraphOp or None): The `GraphOp` object recording the function call which outputted `obj`. If
            `obj` is a leaf, `creator_op = None`.

    Returns:
        (GraphData): A wrapped version of the object, which can be used as if it were unwrapped.
    """
    try:
        obj.xnode_graphdata = GraphData(obj, props_to_surface, creator_op, creator_pos)
    except AttributeError:
        # Built-in types, like `dict` and `list`, cannot have new properties added to them unless subclassed
        class WrapperClass(type(obj)):
            pass
        obj = WrapperClass(obj)
        obj.xnode_graphdata = GraphData(obj, props_to_surface, creator_op, creator_pos)
    return obj

def has_graphdata(obj):
    """Returns `True` if an object has an associated `GraphData` object.

    Objects which are tracked in the computation graph via a call to `track_data()` have `GraphData` objects associated
    with them to contain information about the op which created the object and how it should be rendered.

    Args:
        obj (object): An object which might be tracked (and, thus, have a `GraphData` object).

    Returns:

    """
    try:
        return hasattr(obj, 'xnode_graphdata')
    except NotImplementedError:
        # PyTorch variables throw a NotImplementedError if you do a `getattr` on a non-existent attribute,
        # which breaks hasattr.
        return False

def get_graphdata(obj):
    """Returns the `GraphData` object associated with an object.

    This function assumes that `has_graphdata(obj) == True`.

    Args:
        obj (object): An object which has been tracked

    Returns:

    """
    return getattr(obj, 'xnode_graphdata')

class OpGenerator(wrapt.ObjectProxy):
    """Wraps a callable object, creating a `GraphOp` whenever called and wrapping each output in a `GraphData`."""
    def __init__(self, obj, output_props_to_surface=None):
        """Constructor.

        Args:
            obj (callable): A callable object, such as a function, whose executions should be recorded in the
                computation graph.
            output_props_to_surface (list or None): A dict determining what properties of each `GraphData`
                created by the callable should be surfaced in visualization. The i-th entry of `output_props_to_surface`
                is passed to the i-th output of the callable. For example:
                `
                a, b = Foo(), Foo()  # Foo is any arbitrary class
                a.prop1 = 10
                b.prop2 = 5

                def f(x, y):
                  new_x = Foo()
                  new_x.prop1 += 10
                  new_y = Foo()
                  new_y.prop2 -= 5
                  return new_x, new_y

                c = OpGenerator(f, output_props_to_surface=[{'Arbitrary Value': 'prop1'}, {'Another Value': 'prop2'}])
                x, y = c(a, b)  # x and y are GraphData wrappers around Foo instances
                `
                Now, when `x` is visualized and inspected in the client, it shows "Arbitrary Value": 10. When `y` is
                visualized and inspected, it shows "Another Value": 0. For functions whose outputs have different
                types under different conditions, or may output different numbers of values,
                setting `output_props_to_surface` to `None` will leave all `GraphData` outputs without any fields
                visualized. Fields can then be set after creation with `GraphData.surface_prop_in_client()`.
        """
        super(OpGenerator, self).__init__(obj)
        # wrapt requires all wrapper properties to start with _self_
        self._self_output_props = output_props_to_surface

    def __call__(self, *args, **kwargs):
        """Executes the wrapped function and creates a `GraphOp` recording the execution.

        A single `GraphOp` is created to record the function's execution. The wrapped function should not in its
        internals create any additional `GraphOp` objects; functions which create more `GraphOp` objects should be
        wrapped with `AbstractContainerGenerator` instead.

        Args:
            args (tuple): A sequence of positional arguments to pass to the wrapped function.
            kwargs (dict): Keyword arguments to pass to the wrapped function.

        Returns:
            The outputs of the wrapped function, each wrapped in a `GraphData` instance.
        """
        # TODO: should we dive into sequences and look for nested GraphData? If not, torch.cat doesn't really work
        # (since it takes as argument a list), but could be made to work with a wrapper that takes in any number of
        # inputs and collects them into a list before calling cat. If we do, how do we know when to stop diving,
        # and what do we do about output_props?
        ret = self.__wrapped__(*args, **kwargs)
        multiple_returns = isinstance(ret, tuple) and len(ret) > 1
        op = GraphOp(self.__wrapped__, args, kwargs)
        # TODO handle passthrough returns
        if multiple_returns:
            ret_graphdata = tuple([track_data(r,
                                     self._self_output_props[i] if self._self_output_props is not None else None,
                                     creator_op=op,
                                     creator_pos=i)
                          for i, r in enumerate(ret)])
        else:
            ret_graphdata = track_data(ret,
                              self._self_output_props[0] if self._self_output_props is not None else None,
                              creator_op=op,
                              creator_pos=0)
        op.outputs = ret_graphdata if isinstance(ret_graphdata, tuple) else (ret_graphdata, )
        return ret_graphdata

class AbstractContainerGenerator(wrapt.ObjectProxy):
    """Wraps a callable object, causing all `GraphOp` and `GraphContainer` objects created during its execution to be
    contained in a new abstractive `GraphContainer`."""
    def __call__(self, *args, **kwargs):
        """Executes the wrapped function and nests every `GraphOp` and `GraphContainer` it creates inside a new
        `GraphContainer`.

        `AbstractContainerGenerator` expects that its wrapped function contains (perhaps further nested within other
        functions) some `OpGenerator` functions that will produce `GraphOp` objects when called. Further, it expects
        that at least some of the inputs and outputs of the wrapped function have `GraphData` objects.
        These objects are then used to identify every `GraphOp` that exists between the inputs and the outputs in the
        DAG, and wraps their outermost parents in a new abstractive container.

        Args:
            args (tuple): Positional arguments to the wrapped function.
            kwargs (dict): Keyword arguments to the wrapped function.

        Returns:
            The unchanged output of the wrapped function.
        """
        inputs = set(get_graphdata(obj) for obj in args + tuple(kwargs.values()) if has_graphdata(obj))
        ret = self.__wrapped__(*args, **kwargs)
        output_graphdata = [get_graphdata(obj) for obj in ret if has_graphdata(obj)] \
            if isinstance(ret, tuple) and len(ret) > 1 else [get_graphdata(ret)]
        _build_abstractive_container(output_graphdata, inputs, self.get_fn_name())
        return ret

    def get_fn_name(self):
        try:
            return self.__wrapped__.__name__
        except AttributeError:
            return self.__wrapped__.__class__.__name__

def tick(output, temporal_level):
    """Create a temporal container extending backwards from output and encapsulating any outer-level op or container
    with `temporal_level`.

    Every op and container has a temporal level; abstractive containers and ops have level 0, and temporal containers
    have level > 0. Temporal containers of level n contain only items of temporal level n-1. If
    `output`'s outermost parent (see `Nestable.get_outermost_parent()` for definition) is of temporal level
    lower than  `temporal_level`, then `_tick(output, temporal_level-1)` is called before execution. This ensures that
    the concept of "temporal subdivision" is maintained, and that temporal containers only contain items at exactly one
    level below them.

    Args:
        output (GraphData): A `GraphData` from which to build the new temporal container.
        temporal_level (int): The temporal level of ops and containers that should be encapsulated by the new temporal
            container.
    """
    output = get_graphdata(output)
    if output.creator_op is not None \
            and output.creator_op.get_outermost_parent().temporal_level < temporal_level:
        tick(output, temporal_level - 1)

    contents = set()
    ops_checked = set()
    ops_to_check = deque([output.creator_op])
    max_temporal_step = -1

    while len(ops_to_check) > 0:
        op = ops_to_check.popleft()
        if op not in ops_checked and op is not None:
            ops_checked.add(op)
            highest = op.get_outermost_parent()
            assert highest.temporal_level >= temporal_level
            if highest.temporal_level == temporal_level:
                contents.add(highest)
                ops_to_check.extend([arg.creator_op for arg in op.get_tracked_args()])
            else:
                max_temporal_step = max(max_temporal_step, highest.temporal_step)
    container = GraphContainer(contents, temporal_level=temporal_level+1, temporal_step=max_temporal_step+1)
    # Update container fields of new container's contents after iteration to prevent premature exiting (consider ops
    # op1, op2, which are in temporal container c1 of level 1. We now are adding a temporal container c2 of height 2. If
    # we update the container of c1 during iteration, then when iteration reaches op2, its outermost parent would be
    # c2. Thus,it would not be enqueued and its ancestors would not be added to c2).
    for item in container.contents:
        item.container = container


"""ifParser.py

Usage:
  ifParser.py parse <extension> [--path=<location>] [--recursive | --only=<filename>...]

Commands:
  parse    Go through specified files and change arithmetic IF commands to normal IF THEN statements.

Arguments:
  <extension>    The extension with which glob finds the files to parse through. E.g. '.F90'
  <filename>     Name of the file to parse for arithmetic IF statements, should include the extension.

Options:
  -p --path=<>               The path of the folder or files to parse through. Include last forward slash './path/to/folder/' [default: ./]

  -r --recursive             If specified the program will run recursively

  -o --only <name1,name2>    Only convert the given files or files seperated by comma.Include file extension.For example '-o file1.txt,file2.cpp,file3.f...'

"""

import re
from docopt import docopt
from glob import glob
import fnmatch
import warnings

args = docopt(__doc__, version = '1.0')
#Parser to find arithmetic IF statemetns
ifParser = re.compile(r"^(?!\!).+(IF\s*?\(.*?\))\s*?(\d+),(\d+),(\d+)  ", re.IGNORECASE | re.VERBOSE | re.MULTILINE )

def collectPaths(location = args['--path'], fromType = args['<extension>']):
    """Returns a list of all files in the specified --path with the specified extension <extension>
    """
    globArgument = location + '*%s'%fromType
    if ( args['--recursive'] == True ):
        globArgument = location + '**/*%s'%fromType

    if(len(args['--only']) > 0):
        #args['--only'][0] is the string "file1.txt,file2.cpp..."
        #parses the string to a list of file names
        files = args['--only'][0].split(',')
        #["./location/fileone.f", "./location/filetwo.f", ["./location/" + "filename.f"]
        paths = [location + name for name in files]
        #print("PATHS:\n" + str(paths))
    elif(len(args['--only']) == 0):
        #paths = glob("full/path/filename(.f)<-dot in fromType string")
        paths = glob(globArgument, recursive = args['--recursive'])
    try:
        with open(".gitignore",'r') as file:
            print("Reading gitignore file")
            ignoreInfo = file.readlines()
            for line in ignoreInfo:
                if(line[0] == '#'):
                    ignoreInfo.remove(line)
        for idx, line in enumerate(ignoreInfo):
            ignoreInfo[idx] = line.replace("\n", "*")
            ignoreInfo[idx] = './' + ignoreInfo[idx]
        print(f"IgnoreInfo: {ignoreInfo}")
        #print(f"paths pre filter: {type(paths)} \n{paths}")

        paths = (n for n in paths if not any(fnmatch.fnmatch(n,ignore) for ignore in ignoreInfo))

        holder = []
        for path in paths:
            holder.append(path)
        #print(f"Holder paths: {holder}")
        paths = holder
        #print(f"returned paths: {paths}")
    except Exception:
        warnings.warn("Warning: No .gitignore file found, cannot exclude paths not under version control in current folder")
    return paths

def ifWriter(indent: int, statement: str, value1: str, value2: str, value3: str) -> str:
    """indent: Indentation of the statement at begining of line. The statement is not returned indented, it should be overwritten at the start of its current position.

    statement: Arithmetic IF statement to rewrite ('IF(expresion)').

    value1, value2, value3: Values of arithmetic IF for < 0, == 0, > 0 respectively.

    Returns a string to overwrite the file string between the start index and end index of the current arithmetic If expression filestring[ match.start() : match.end()]"""
    #Depending on the regex used to parse the file the first IF() statement might not need to be indented. If Multiline is used in the parser, you need indentation, else you don't.
    returnStr = ''
    #if v1 == v2  IF (A <= 0)
    #group(1)[:-1] + '<= 0)'
    #elif V2 == v3 IF (a <  0)
    #else IF(a <0) IF (a == 0) if (a > 0)
    if(value1 == value2):
        #Less or equal to 0
        statement = statement[:-1] + ' <= 0) THEN\n'

        returnStr = f"{statement}" + " "*(indent + 2) + f"GOTO {value1}\n" + " "*indent + "ELSE\n" + " "*(indent + 2) + f"GOTO {value3}\n"+ " "*indent +"END IF\n"
    elif(value2 == value3):
        statement = statement[:-1] + ' < 0) THEN\n'

        returnStr = f"{statement}" + " "*(indent + 2) + f"GOTO {value1}\n" + " "*indent + "ELSE\n" + " "*(indent + 2) + f"GOTO {value3}\n"+ " "*indent +"END IF\n"
    else:
        statement1 = statement[:-1] + ' < 0) THEN\n'
        statement2 = statement[:-1] + ' == 0) THEN\n'

        returnStr = f"{statement1}" + " "*(indent + 2) + f"GOTO {value1}\n" + " "*indent + f"ELSE {statement2}" + " "*(indent + 2) + f"GOTO {value2}\n" + " "*indent +"ELSE\n" + " "*(indent + 2) + f"GOTO {value3}\n" + " "*indent + "END IF\n"
    return returnStr

if __name__ == '__main__':
    if(args['parse']):
        filesToParse = collectPaths()
        for filePath in filesToParse:
            print(f"Parsed: {filesToParse.index(filePath) + 1} / {len(filesToParse)} files")
            with open(filePath, 'r') as file:
                #print(f"Parsing {filePath}")
                fileString = file.read()

            matches = ifParser.finditer(fileString)
            ifList = []
            for matchNum , match in enumerate(matches):
                indent = match.start(1) - fileString[:match.start(1)].rfind("\n") - 1

                ifStatementString = ifWriter(indent, match.group(1), match.group(2), match.group(3), match.group(4))

                ifList.append([match.start(1),match.end(4), ifStatementString])
            #####################
            ## TODO : Use insertStrAtIndecies function
            ## TODO : remove setting writeString inside the with open, keep it clean
            accumulator = 0
            writeString = fileString
            for statement in ifList:
                with open(filePath, 'w') as file:
                    writeString = writeString[:statement[0] + accumulator] + statement[2] + writeString[statement[1] + accumulator:]
                    file.write(writeString)
                accumulator += len(statement[2]) - (statement[1] - statement[0])
#%% test fnmatch


import xml.etree.ElementTree as ET

tree = ET.parse('./annotations.xml')
root = tree.getroot()

#изменить порядок id
arr = []
for elem in root.iter('image'):
    arr.append(elem.attrib.get('id'))

arr.reverse()
num_id = 0

for elem in root.iter('image'):
    elem.set('id', arr[num_id])
    num_id += 1

#Изменение формата файлов
for elem in root.iter('image'):
    string = elem.attrib.get('name')
    edited = string.replace('.jpg', '.png')
    elem.set('name', edited)

#Удаление пути в названии файла
for elem in root.iter('image'):
    string = elem.attrib.get('name')
    n = string.rfind('/')
    edited = string[(n + 1):]
    elem.set('name', edited)

tree.write('newitems.xml', encoding="UTF-8")


# coding=utf-8
import datetime
import requests
import requests.exceptions
from flask import current_app
from sqlalchemy.orm.exc import NoResultFound

from src.interface_adapter.sql.model.database import Database as Db
from src.interface_adapter.sql.model.models import NainA
from src.util.env import is_development_environment

ADH6_USER = "adh6_user"
ADH6_ADMIN = "adh6_admin"

TESTING_CLIENT = 'TestingClient'

def token_info(access_token) -> dict:
    if access_token.startswith("NAINA_"):
        return authenticate_temp_account(access_token[6:])

    if current_app.config["TESTING"]:
        return {
            "uid": TESTING_CLIENT,
            "scope": ["profile"],
            "groups": []
        }
    return authenticate_against_sso(access_token)

def authenticate_temp_account(access_token):
    s = Db.get_db().get_session()

    q = s.query(NainA)
    q = q.filter(NainA.access_token == access_token)
    try:
        naina = q.one()
        now = datetime.datetime.now()
        if naina.expiration_time > now > naina.start_time:  # Make sure the token is still valid.
            return {
                "uid": f"TEMP_ACCOUNT({naina.id})[{naina.first_name} {naina.last_name}]",
                "scope": ["profile"],
                "groups": [ADH6_USER]
            }
        else:
            return None  # Expired token.
    except NoResultFound:
        return None  # No token found.
    finally:
        s.close()

def get_sso_groups(token):
    try:
        verify_cert = True
        if is_development_environment():
            verify_cert = False

        headers = {"Authorization": "Bearer " + token}
        r = requests.get(
            current_app.config["AUTH_SERVER_ADDRESS"],
            headers=headers,
            timeout=1,
            verify=verify_cert
        )
    except requests.exceptions.ReadTimeout:
        return None
    if r.status_code != 200 or "id" not in r.json():
        return None

    result = r.json()
    if is_development_environment():
        result["groups"] = [ADH6_USER, ADH6_ADMIN]  # If we are testing, consider the user asg.admin
    return result

def authenticate_against_sso(access_token):
    infos = get_sso_groups(access_token)
    if not infos:
        return None
    return {
        "uid": infos["id"],
        "scope": ["profile"],
        "groups": infos["groups"]
    }


from django.core.exceptions import ValidationError
from django.db.utils import ProgrammingError
from django.forms import ModelForm
from django.utils.translation import gettext as _

from crispy_forms.layout import Field, Layout, Submit
from crispy_forms.helper import FormHelper
from crispy_forms.bootstrap import StrictButton, FieldWithButtons

from .models import Auction, Bid, Comment

class NewAuctionForm(ModelForm):
    class Meta:
        model = Auction
        fields = ("title", "description", "image", "starting_bid", "categories")

    def __init__(self, *args, **kwargs):
        action = kwargs.pop("action")
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_id = "id_create_auction_form"
        self.helper.form_action = action
        self.helper.form_method = "post"
        self.helper.add_input(Submit("submit", "Submit", css_class="col-2 offset-md-10"))
        self.helper.form_class = "form-horizontal"
        self.helper.label_class = "col-2"
        self.helper.field_class = "col-10"

class NewBidForm(ModelForm):
    class Meta:
        model = Bid
        fields = ("value",)

    def __init__(self, *args, **kwargs):
        bid_price = kwargs.pop("bid_price", None)
        action = kwargs.pop("action", None)
        invalid_style = kwargs.pop("invalid_style", None)
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_id = "id_new_bid_form"
        self.helper.form_action = action
        self.helper.form_method = "post"
        self.helper.form_class = "form-inline"
        self.helper.label_class = "mr-2"
        self.helper.form_show_labels = False
        self.helper.layout = Layout(
            FieldWithButtons(
                Field("value", placeholder=bid_price, css_class="is-invalid" if invalid_style else ""),
                Submit("submit", "Bid",css_class="btn btn-primary"),
            )
        )

    def clean_value(self):
        if not self.instance:
            raise ProgrammingError("There is no instance with this form")
        if self.cleaned_data["value"] <= self.instance.auction.highest_bid_value:
            raise ValidationError(_("Bid value is lower then the highest bid value"))
        if self.instance.bidder == self.instance.auction.owner:
            raise ValidationError(_("You can't bid on your own auction"))
        return self.cleaned_data["value"]

class NewCommentFrom(ModelForm):
    class Meta:
        model = Comment
        fields = ("content",)

    def __init__(self, *args, **kwargs):
        action = kwargs.pop("action", None)
        super().__init__(*args, **kwargs)
        self.fields["content"].widget.attrs.update({"required": False, "placeholder": _("Leave a comment")})
        self.helper = FormHelper()
        self.helper.form_id = "id_new_comment_form"
        self.helper.form_action = action
        self.helper.form_method = "post"
        self.helper.form_show_labels = False

        self.helper.layout = Layout(
            "content",
            Submit("submit", "Comment", css_class="btn btn-primary"),
        )