import pytest
import json
import brownie
import math
import eth_abi
from brownie import Contract, accounts, interface, Wei, NotionalV2FlashLiquidator
from brownie.network.state import Chain
from brownie.convert import to_bytes
from scripts.CTokenMigrationEnvironment import cTokenMigrationEnvironment
from tests.helpers import get_balance_action

chain = Chain()

wfCashABI = json.load(open("abi/WrappedfCash.json"))
wfCashFactoryABI = json.load(open("abi/WrappedfCashFactory.json"))

@pytest.fixture(scope="module", autouse=True)
def run_around_tests():
    chain.snapshot()
    yield
    chain.revert()

@pytest.fixture(autouse=True)
def isolation(fn_isolation):
    pass

@pytest.fixture(scope="module", autouse=True)
def env(accounts):
    return cTokenMigrationEnvironment(accounts[0])

def snapshot_invariants(env, currencyId):
    snapshot = {}
    snapshot["assetRate"] = env.notional.getCurrencyAndRates(currencyId)["assetRate"]["rate"]
    return snapshot

def check_invariants(env, snapshot, currencyId):
    # Final asset rate shouldn't move once set
    assert snapshot["assetRate"] == env.notional.getCurrencyAndRates(currencyId)["assetRate"]["rate"]

def test_donation_does_not_affect_contract(env, accounts):
    env.deployNCTokens()
    env.migrateAll()
    assetRateBefore = env.ncTokens[1].getExchangeRateStateful({"from": accounts[0]}).return_value
    accounts[0].transfer(env.ncTokens[1], 100e18)
    assetRateAfter = env.ncTokens[1].getExchangeRateStateful({"from": accounts[0]}).return_value
    assert assetRateAfter == assetRateBefore

    assert env.ncTokens[1].balanceOf(accounts[0]) == 0
    env.ncTokens[1].mint({"value": 10e18, "from": accounts[0]})
    assert pytest.approx(
        Wei(env.ncTokens[1].balanceOf(accounts[0])) * Wei(env.ncTokens[1].getExchangeRateView()) / Wei(1e18), rel=1e10
    ) == 10e18

def test_only_notional_can_initialize(env, accounts):
    env.deployNCTokens()

    with brownie.reverts():
        env.ncTokens[1].initialize(1e18, {"from": accounts[0]})

def test_wrapper_names(env):
    env.deployNCTokens()
    # Migration is required for initialization
    env.migrateAll()
    assert env.ncTokens[1].name() == 'Notional Wrapped Ether'
    assert env.ncTokens[2].name() == 'Notional Wrapped Dai Stablecoin'
    assert env.ncTokens[3].name() == 'Notional Wrapped USD Coin'
    assert env.ncTokens[4].name() == 'Notional Wrapped Wrapped BTC'

    assert env.ncTokens[1].symbol() == 'nwETH'
    assert env.ncTokens[2].symbol() == 'nwDAI'
    assert env.ncTokens[3].symbol() == 'nwUSDC'
    assert env.ncTokens[4].symbol() == 'nwWBTC'

def test_safety_check_locks_contract(env, accounts):
    env.deployNCTokens()
    # Migration is required for initialization
    env.migrateAll()

    underlying = interface.IERC20(env.notional.getCurrencyAndRates(2)["underlyingToken"][0])
    asset = interface.nwTokenInterface(env.notional.getCurrencyAndRates(2)["assetToken"][0])
    # Simulate an attack that somehow takes DAI
    underlying.transfer(accounts[0], 100e18, {"from": env.ncTokens[2]})

    # Assert that minting and redeeming is no longer possible
    underlying.approve(asset, 2 ** 256 - 1, {"from": env.whales["DAI"]})
    with brownie.reverts("Invariant Failed"):
        asset.mint(100e18, {"from": env.whales["DAI"]})

    with brownie.reverts("Invariant Failed"):
        asset.redeem(100e8, {"from": env.notional})

    with brownie.reverts("Invariant Failed"):
        asset.redeemUnderlying(100e8, {"from": env.notional})

def test_can_redeem_all_tokens(env):
    env.deployNCTokens()
    env.migrateAll()

    for i in range(1, 5):
        if i != 1:
            underlying = interface.IERC20(env.notional.getCurrencyAndRates(i)["underlyingToken"][0])
        asset = interface.IERC20(env.notional.getCurrencyAndRates(i)["assetToken"][0])
        nwAsset = interface.nwTokenInterface(env.notional.getCurrencyAndRates(i)["assetToken"][0])

        balanceOfAssetBefore = underlying.balanceOf(asset.address) if i != 1 else asset.balance()
        balanceOfNotionalBefore = underlying.balanceOf(env.notional.address) if i != 1 else env.notional.balance()

        nwAsset.redeem(asset.balanceOf(env.notional), {"from": env.notional})

        remainingAssetBalance = underlying.balanceOf(asset.address) if i != 1 else asset.balance()
        # Assert that only dust remains at a full redemption
        if i == 1 or underlying.decimals() == 18:
            assert remainingAssetBalance < 3e10
        elif underlying.decimals() == 8:
            assert remainingAssetBalance < 5
        elif underlying.decimals() == 6:
            assert remainingAssetBalance < 2

        balanceOfNotionalAfter = underlying.balanceOf(env.notional.address) if i != 1 else env.notional.balance()

        assert balanceOfAssetBefore - remainingAssetBalance + balanceOfNotionalBefore == balanceOfNotionalAfter

def test_redeem_underlying_to_zero(env):
    env.deployNCTokens()
    env.migrateAll()

    for i in range(1, 5):
        if i != 1:
            underlying = interface.IERC20(env.notional.getCurrencyAndRates(i)["underlyingToken"][0])
        asset = interface.IERC20(env.notional.getCurrencyAndRates(i)["assetToken"][0])
        nwAsset = interface.nwTokenInterface(env.notional.getCurrencyAndRates(i)["assetToken"][0])

        balanceOfAssetBefore = underlying.balanceOf(asset.address) if i != 1 else asset.balance()
        balanceOfNotionalBefore = underlying.balanceOf(env.notional.address) if i != 1 else env.notional.balance()

        with brownie.reverts("ERC20: burn amount exceeds balance"):
            nwAsset.redeemUnderlying(balanceOfAssetBefore, {"from": env.notional})

        # Assert that only dust remains at a full redemption
        if i == 1 or underlying.decimals() == 18:
            dust = 1e10
        elif underlying.decimals() == 8:
            dust = 1
        elif underlying.decimals() == 6:
            dust = 1

        nwAsset.redeemUnderlying(balanceOfAssetBefore - dust, {"from": env.notional})

        remainingAssetBalance = underlying.balanceOf(asset.address) if i != 1 else asset.balance()
        balanceOfNotionalAfter = underlying.balanceOf(env.notional.address) if i != 1 else env.notional.balance()

        assert remainingAssetBalance == dust
        assert balanceOfAssetBefore - remainingAssetBalance + balanceOfNotionalBefore == balanceOfNotionalAfter

def deposit_underlying(env, account, currencyId, amount):
    value = 0
    if currencyId == 1:
        value = amount
        accountBalanceBefore = account.balance()
    else:
        token = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
        token.approve(env.notional, 2**256-1, {"from": account})
        accountBalanceBefore = token.balanceOf(account)

    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "DepositUnderlying", depositActionAmount=amount)],
        {"from": account, "value": value}
    )
    cashBalanceBefore = env.notional.getAccountBalance(currencyId, account)["cashBalance"]

    if currencyId == 1:
        deposited = accountBalanceBefore - account.balance()
    else:
        deposited = accountBalanceBefore - token.balanceOf(account)

    chain.undo()
    env.deployNCTokens()
    env.migrateAll()

    snapshot = snapshot_invariants(env, currencyId)

    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "DepositUnderlying", depositActionAmount=amount)],
        {"from": account, "value": value}
    )
    cashBalanceAfter = env.notional.getAccountBalance(currencyId, account)["cashBalance"]
    if currencyId == 1:
        assert deposited == accountBalanceBefore - account.balance()
    else:
        assert deposited == accountBalanceBefore - token.balanceOf(account)

    assert pytest.approx(cashBalanceBefore, rel=1e-7) == cashBalanceAfter
    check_invariants(env, snapshot, currencyId)

def deposit_asset(env, account, currencyId, amount):
    if currencyId == 1:
        cToken = interface.CEtherInterface(env.notional.getCurrencyAndRates(currencyId)["assetToken"][0])
        cToken.mint({"from": account, "value": amount})
    else:
        cToken = interface.CErc20Interface(env.notional.getCurrencyAndRates(currencyId)["assetToken"][0])
        underlying = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
        underlying.approve(cToken, 2**256-1, {"from": account})
        cToken.mint(amount, {"from": account})

    assetToken = interface.IERC20(cToken.address)
    assetToken.approve(env.notional, 2**256-1, {"from": account})
    accountBalanceBefore = assetToken.balanceOf(account)

    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "DepositAsset", depositActionAmount=assetToken.balanceOf(account))],
        {"from": account}
    )
    cashBalanceBefore = env.notional.getAccountBalance(currencyId, account)["cashBalance"]
    deposited = accountBalanceBefore - assetToken.balanceOf(account)

    chain.undo()

    env.deployNCTokens()
    env.migrateAll()

    nwToken = interface.nwTokenInterface(env.notional.getCurrencyAndRates(currencyId)["assetToken"][0])
    if currencyId == 1:
        nwToken.mint({"from": account, "value": amount})
    else:
        underlying = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
        underlying.approve(nwToken, 2**256-1, {"from": account})
        nwToken.mint(amount, {"from": account})

    assetToken = interface.IERC20(nwToken.address)
    assetToken.approve(env.notional, 2**256-1, {"from": account})
    accountBalanceBefore = assetToken.balanceOf(account)
    snapshot = snapshot_invariants(env, currencyId)
    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "DepositAsset", depositActionAmount=assetToken.balanceOf(account))],
        {"from": account}
    )
    cashBalanceAfter = env.notional.getAccountBalance(currencyId, account)["cashBalance"]
    assert pytest.approx(deposited, rel=1e-7) == accountBalanceBefore - assetToken.balanceOf(account)
    assert pytest.approx(cashBalanceBefore, rel=1e-7) == cashBalanceAfter

    check_invariants(env, snapshot, currencyId)

def redeem_underlying(env, account, currencyId, amount):
    value = 0
    if currencyId == 1:
        value = amount
    else:
        token = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
        token.approve(env.notional, 2**256-1, {"from": account})

    env.deployNCTokens()
    env.migrateAll()
    snapshot = snapshot_invariants(env, currencyId)

    # Clear any residual cash balance from previous tests
    if env.notional.getAccountBalance(currencyId, account)["cashBalance"] > 0:
        env.notional.batchBalanceAction(
            account, [get_balance_action(currencyId, "None", withdrawEntireCashBalance=True, redeemToUnderlying=True)],
            {"from": account}
        )

    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "DepositUnderlying", depositActionAmount=amount)],
        {"from": account, "value": value}
    )
    cashBalance = env.notional.getAccountBalance(currencyId, account)["cashBalance"]

    if currencyId == 1:
        accountBalanceBefore = account.balance()
    else:
        accountBalanceBefore = token.balanceOf(account)

    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "None", withdrawAmountInternalPrecision=Wei(cashBalance / 2), redeemToUnderlying=True)],
        {"from": account}
    )
    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "None", withdrawEntireCashBalance=True, redeemToUnderlying=True)],
        {"from": account}
    )

    if currencyId == 1:
        assert pytest.approx(amount, rel=1e-7, abs=3) == account.balance() - accountBalanceBefore
    else:
        assert pytest.approx(amount, rel=1e-7, abs=3) == token.balanceOf(account) - accountBalanceBefore

    assert env.notional.getAccountBalance(currencyId, account)["cashBalance"] == 0
    check_invariants(env, snapshot, currencyId)

def redeem_asset(env, account, currencyId, amount):
    value = 0
    if currencyId == 1:
        value = amount
    else:
        token = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
        token.approve(env.notional, 2**256-1, {"from": account})

    env.deployNCTokens()
    env.migrateAll()
    snapshot = snapshot_invariants(env, currencyId)
    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "DepositUnderlying", depositActionAmount=amount)],
        {"from": account, "value": value}
    )
    cashBalance = env.notional.getAccountBalance(currencyId, account)["cashBalance"]

    nwToken = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["assetToken"][0])
    accountBalanceBefore = nwToken.balanceOf(account)

    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "None", withdrawAmountInternalPrecision=Wei(cashBalance / 2), redeemToUnderlying=False)],
        {"from": account}
    )
    env.notional.batchBalanceAction(
        account, [get_balance_action(currencyId, "None", withdrawEntireCashBalance=True, redeemToUnderlying=False)],
        {"from": account}
    )

    assert cashBalance == nwToken.balanceOf(account) - accountBalanceBefore
    assert env.notional.getAccountBalance(currencyId, account)["cashBalance"] == 0
    check_invariants(env, snapshot, currencyId)

def wrapped_fcash_mint_via_underlying(env, account, currencyId, fCashAmount):
    markets = env.notional.getActiveMarkets(currencyId)
    wfCashFactory = Contract.from_abi("wfCash Factory", "0x5D051DeB5db151C2172dCdCCD42e6A2953E27261", wfCashFactoryABI)
    wfCashAddress = wfCashFactory.deployWrapper(currencyId, markets[0][1], {"from": accounts[1]}).return_value
    wfCash = Contract.from_abi('wfCash', wfCashAddress, wfCashABI)

    env.deployNCTokens()
    env.migrateAll()
    snapshot = snapshot_invariants(env, currencyId)

    if currencyId != 1:
        token = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])

    amount = math.floor(wfCash.previewMint(fCashAmount) * 1.000001)

    # wfETH uses WETH
    WETH = interface.WETH9("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
    if currencyId == 1:
        WETH.deposit({"from": account, "value": amount})
        token = interface.IERC20(WETH.address)

    accountBalanceBefore = token.balanceOf(account)
    token.approve(wfCash, 2**256-1, {"from": account})

    wfCash.mintViaUnderlying(amount, fCashAmount, account, 0, {"from": account})

    if currencyId == 1:
        assert token.balanceOf(account) < 1e13
    else:
        assert pytest.approx(token.balanceOf(account), rel=1e-7) == accountBalanceBefore - amount
    assert wfCash.balanceOf(account) == fCashAmount

    # Test redemption:
    accountBalanceBefore = token.balanceOf(account)
    wfCash.redeemToUnderlying(fCashAmount, account, 0, {"from": account})
    accountBalanceAfter = token.balanceOf(account)
    assert pytest.approx((accountBalanceAfter - accountBalanceBefore) / amount, rel=5e-4) == 0.9985

    check_invariants(env, snapshot, currencyId)

def wrapped_fcash_mint_via_asset(env, account, currencyId, fCashAmount):
    env.deployNCTokens()
    env.migrateAll()
    snapshot = snapshot_invariants(env, currencyId)

    markets = env.notional.getActiveMarkets(currencyId)
    wfCashFactory = Contract.from_abi("wfCash Factory", "0x5D051DeB5db151C2172dCdCCD42e6A2953E27261", wfCashFactoryABI)
    txn = wfCashFactory.deployWrapper(currencyId, markets[0][1], {"from": accounts[1]})
    wfCashAddress = txn.return_value
    wfCash = Contract.from_abi('wfCash', wfCashAddress, wfCashABI)

    token = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["assetToken"][0])
    amount = math.floor(wfCash.previewMint(fCashAmount) * 1.0001)
    nwToken = interface.nwTokenInterface(token.address)
    if currencyId == 1:
        nwToken.mint({"value": amount, "from": account})
    else:
        underlying = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
        underlying.approve(nwToken, 2 ** 256 - 1, {"from": account})
        nwToken.mint(amount, {"from": account})

    accountBalanceBefore = token.balanceOf(account)
    token.approve(wfCash, 2**256-1, {"from": account})

    # This only works for newly deployed wrappers
    if 'WrapperDeployed' in txn.events:
        wfCash.mintViaAsset(accountBalanceBefore, fCashAmount, account, 0, {"from": account})

        assert token.balanceOf(account) < 1e8
        assert wfCash.balanceOf(account) == fCashAmount

        wfCash.redeemToAsset(fCashAmount, account, 0, {"from": account})
        accountBalanceAfter = token.balanceOf(account)
        assert pytest.approx((accountBalanceAfter/ accountBalanceBefore), rel=5e-4) == 0.9985
    else:
        with brownie.reverts("ERC20: insufficient allowance"):
            wfCash.mintViaAsset(accountBalanceBefore, fCashAmount, account, 0, {"from": account})

    check_invariants(env, snapshot, currencyId)

def mint_ntoken_underlying(env, account, currencyId, depositAmount):
    env.deployNCTokens()
    env.migrateAll()

    if currencyId == 1:
        balanceBefore = account.balance()
    else:
        token = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
        token.approve(env.notional, 2**256-1, {"from": account})
        balanceBefore = token.balanceOf(account)

    env.notional.batchBalanceAction(
        account, [
            get_balance_action(
                currencyId,
                "DepositUnderlyingAndMintNToken",
                depositActionAmount=depositAmount
            )
        ], {"from": account, "value": depositAmount if currencyId == 1 else 0}
    )
    nTokenBalance = env.notional.getAccountBalance(currencyId, account)['nTokenBalance']
    assert nTokenBalance > 0

    if currencyId == 1:
        balanceAfter = account.balance()
    else:
        token = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
        balanceAfter = token.balanceOf(account)

    assert balanceBefore - balanceAfter == depositAmount

    env.notional.batchBalanceAction(
        account, [
            get_balance_action(
                currencyId,
                "RedeemNToken",
                depositActionAmount=nTokenBalance,
                withdrawEntireCashBalance=True,
                redeemToUnderlying=True
            )
        ], {"from": account}
    )
    nTokenBalance = env.notional.getAccountBalance(currencyId, account)['nTokenBalance']
    assert nTokenBalance == 0

    if currencyId == 1:
        balanceAfter = account.balance()
    else:
        token = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
        balanceAfter = token.balanceOf(account)

    assert 0 < balanceBefore - balanceAfter and balanceBefore - balanceAfter < depositAmount * 0.0003

def test_deposit_underlying_eth(env):
    deposit_underlying(env, accounts[0], 1, 10e18)

def test_deposit_underlying_dai(env):
    deposit_underlying(env, env.whales["DAI"], 2, 10000e18)

def test_deposit_underlying_usdc(env):
    deposit_underlying(env, env.whales["USDC"], 3, 10000e6)

def test_deposit_underlying_wbtc(env):
    deposit_underlying(env, env.whales["WBTC"], 4, 1e8)

def test_deposit_asset_eth(env):
    deposit_asset(env, accounts[0], 1, 10e18)

def test_deposit_asset_dai(env):
    deposit_asset(env, env.whales["DAI"], 2, 10000e18)

def test_deposit_asset_usdc(env):
    deposit_asset(env, env.whales["USDC"], 3, 10000e6)

def test_deposit_asset_wbtc(env):
    deposit_asset(env, env.whales["WBTC"], 4, 1e8)

def test_redeem_underlying_eth(env):
    redeem_underlying(env, accounts[0], 1, 10e18)

def test_redeem_underlying_dai(env):
    redeem_underlying(env, env.whales["DAI"], 2, 10000e18)

def test_redeem_underlying_usdc(env):
    redeem_underlying(env, env.whales["USDC"], 3, 10000e6)

def test_redeem_underlying_wbtc(env):
    redeem_underlying(env, env.whales["WBTC"], 4, 1e8)

def test_redeem_asset_eth(env):
    redeem_asset(env, accounts[0], 1, 10e18)

def test_redeem_asset_dai(env):
    redeem_asset(env, env.whales["DAI"], 2, 10000e18)

def test_redeem_asset_usdc(env):
    redeem_asset(env, env.whales["USDC"], 3, 10000e6)

def test_redeem_asset_wbtc(env):
    redeem_asset(env, env.whales["WBTC"], 4, 1e8)

def test_wrapped_fcash_underlying_eth(env):
    wrapped_fcash_mint_via_underlying(env, accounts[0], 1, 10e8)

def test_wrapped_fcash_underlying_dai(env):
    wrapped_fcash_mint_via_underlying(env, env.whales['DAI'], 2, 100e8)

def test_wrapped_fcash_underlying_usdc(env):
    wrapped_fcash_mint_via_underlying(env, env.whales['USDC'], 3, 100e8)

def test_wrapped_fcash_underlying_wbtc(env):
    wrapped_fcash_mint_via_underlying(env, env.whales['WBTC'], 4, 0.01e8)

def test_wrapped_fcash_asset_eth(env):
    wrapped_fcash_mint_via_asset(env, accounts[0], 1, 10e8)

def test_wrapped_fcash_asset_dai(env):
    wrapped_fcash_mint_via_asset(env, env.whales['DAI'], 2, 100e8)

def test_wrapped_fcash_asset_usdc(env):
    wrapped_fcash_mint_via_asset(env, env.whales['USDC'], 3, 100e8)

def test_wrapped_fcash_asset_wbtc(env):
    wrapped_fcash_mint_via_asset(env, env.whales['WBTC'], 4, 0.01e8)

def test_mint_and_redeem_neth(env):
    mint_ntoken_underlying(env, accounts[0], 1, 10e18)

def test_mint_and_redeem_ndai(env):
    mint_ntoken_underlying(env, env.whales['DAI'], 2, 1_000e18)

def test_mint_and_redeem_nusdc(env):
    mint_ntoken_underlying(env, env.whales['USDC'], 3, 1_000e6)

def test_mint_and_redeem_nwbtc(env):
    mint_ntoken_underlying(env, env.whales['WBTC'], 4, 0.01e8)

def test_no_lost_tokens_due_to_redeem_asset(env):
    currencyId = 2
    fCashAmount = 100e8
    account = env.whales['DAI']

    markets = env.notional.getActiveMarkets(currencyId)
    wfCashFactory = Contract.from_abi("wfCash Factory", "0x5D051DeB5db151C2172dCdCCD42e6A2953E27261", wfCashFactoryABI)
    txn = wfCashFactory.deployWrapper(currencyId, markets[0][1], {"from": accounts[1]})
    wfCashAddress = txn.return_value
    wfCash = Contract.from_abi('wfCash', wfCashAddress, wfCashABI)

    amount = math.floor(wfCash.previewMint(fCashAmount) * 1.000001)
    underlying = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["underlyingToken"][0])
    underlying.approve(wfCash, 2**256-1, {"from": account})
    wfCash.mintViaUnderlying(amount, fCashAmount, account, 0, {"from": account})

    # Wrapper is already deployed
    assert 'WrapperDeployed' not in txn.events

    # Migrate, now the wfCash asset token is wrong?
    env.deployNCTokens()
    env.migrateAll()
    snapshot = snapshot_invariants(env, currencyId)

    assetToken = interface.IERC20(env.notional.getCurrencyAndRates(currencyId)["assetToken"][0])

    # Assert that the account does receive asset tokens
    assert assetToken.balanceOf(account) == 0
    wfCash.redeemToAsset(fCashAmount, account, 0, {"from": account})
    assert wfCash.getAssetToken()[0] == assetToken.address
    assert assetToken.balanceOf(account) > 4400e8

    check_invariants(env, snapshot, currencyId)

def getAssetExchangeRateAddress(env, currencyId):
    assetExchangeRateAddress = env.notional.getRateStorage(currencyId)[1][0]
    return assetExchangeRateAddress

def getAssetExchangeRate(env, currencyId):
    assetExchangeRateAddress = getAssetExchangeRateAddress(env, currencyId)
    assetExchangeRateDecimals = env.notional.getRateStorage(currencyId)[1][1]
    assetExchangeRateDecimals = pow(10, int(assetExchangeRateDecimals))
    assetExchangeRate = interface.AssetRateAdapter(assetExchangeRateAddress).getExchangeRateView()
    assetExchangeRate = assetExchangeRate / assetExchangeRateDecimals
    return assetExchangeRate / 1e10 # Decimals to be set as a variable

def underlyingPrecision(env, currencyId):
    if (currencyId == 1):
        return 1e18
    else:
        params = env.notional.getCurrency(currencyId)[1]
        underlyingDecimals = params[2]
        return underlyingDecimals

def pathCalldataExactOut(fromAddr, toAddr):
    packedEncoder = eth_abi.codec.ABIEncoder(eth_abi.registry.registry_packed)
    return packedEncoder.encode_abi(
        ["address", "uint24", "address", "uint24", "address"],
        [toAddr, 3000, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 3000, fromAddr]
    )

def collateralCalldata(
    env,
    localCurrency,
    account,
    collateralCurrency,
    amount,
    liquidator
):
    router = interface.ISwapRouter("0xE592427A0AEce92De3Edee1F18E0157C05861564")
    localUnderlying = env.notional.getCurrencyAndRates(localCurrency)["underlyingToken"][0]
    collateralUnderlying = env.notional.getCurrencyAndRates(collateralCurrency)["underlyingToken"][0]
    liqCalldata = eth_abi.encode_abi(
        ['(address,uint16,address,uint16,address,address,uint128,uint96,(address,bytes))'],
        [[
            account,
            localCurrency,
            localUnderlying,
            collateralCurrency,
            env.notional.getCurrencyAndRates(collateralCurrency)["assetToken"][0],
            collateralUnderlying,
            0,
            0,
            [
                router.address,
                to_bytes(router.exactOutput.encode_input([
                    pathCalldataExactOut(collateralUnderlying, localUnderlying),
                    liquidator,
                    chain.time() + 20000,
                    math.floor(amount * 1.001),
                    Wei(2**256-1)
                ]), "bytes")
            ]
        ]]
    )
    return eth_abi.encode_abi(
        ['(uint8,bool,bool,bytes)'],
        [[1, False, False, liqCalldata]]
    )

def test_liquidation_eth(env):
    env.deployNCTokens()
    env.migrateAll()

    liquidator = NotionalV2FlashLiquidator.deploy(
        env.notional,
        "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9", # Aave
        "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",  # WETH
        "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", # wstETH
        env.deployer,
        "0xE592427A0AEce92De3Edee1F18E0157C05861564",  # UniV3,
        "0xE592427A0AEce92De3Edee1F18E0157C05861564",  # UniV3,
        {"from": env.deployer}
    )
    liquidator.enableCurrencies([1,2,3,4], {"from": env.deployer})

    env.notional.updateETHRate(
        2,
        "0x6085B0a8f4c7ffA2E8CA578037792D6535d1E29B",
        False,
        130,
        75,
        120,
        {"from": env.notional.owner()}
    )

    localCurrencyRequired = env.notional.calculateCollateralCurrencyLiquidation.call(
        "0x940d92f24547a87ea4fd59d5c78a842bee41bb57",
        3,
        2,
        0,
        0,
        {"from": env.deployer}
    )[0]
    loanAmount = localCurrencyRequired * underlyingPrecision(env, 3) * getAssetExchangeRate(env, 3) * 1.2 / 1e8

    ret = liquidator.flashLoan.call(
        "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
        loanAmount,
        collateralCalldata(
            env,
            3,
            "0x940d92f24547a87ea4fd59d5c78a842bee41bb57",
            2,
            loanAmount,
            liquidator
        ), "0x6b175474e89094c44da98b954eedeac495271d0f"
    )

    assert pytest.approx(ret[0], rel=1e-2) == 22659514797
    assert pytest.approx(ret[1], rel=1e-2) == 1685441876267557686652


# A simplified ResNet for patch classification in order to test end-to-end training procedure.

import torch
import torch.nn as nn

def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
                     padding=dilation, groups=groups, bias=False, dilation=dilation)

def conv1x1(in_planes, out_planes, stride=1):
    return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)

class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
                 base_width=64, dilation=1):
        super(BasicBlock, self).__init__()
        if groups != 1 or base_width != 64:
            raise ValueError('BasicBlock only supports groups=1 and base_width=64')
        if dilation > 1:
            raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
        # Both self.conv1 and self.downsample layers downsample the input when stride != 1
        self.conv1 = conv3x3(inplanes, planes, stride)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = conv3x3(planes, planes)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        identity = x
        out = self.conv1(x)
        out = self.relu(out)
        out = self.conv2(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity
        out = self.relu(out)
        return out

# Simplified ResNet without Batch Normalization layers and an optional thinning argument
# to reduce the number of filters in each layer.
class ResNetSimple(nn.Module):
    def __init__(self, block, layers, num_classes=1000,
                 groups=1, width_per_group=64, replace_stride_with_dilation=None,
                 thin=1):
        super(ResNetSimple, self).__init__()

        self.inplanes = 64
        self.dilation = 1
        if replace_stride_with_dilation is None:
            # each element in the tuple indicates if we should replace
            # the 2x2 stride with a dilated convolution instead
            replace_stride_with_dilation = [False, False, False]
        if len(replace_stride_with_dilation) != 3:
            raise ValueError("replace_stride_with_dilation should be None "
                             "or a 3-element tuple, got {}".format(replace_stride_with_dilation))
        self.groups = groups
        self.base_width = width_per_group
        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
                               bias=False)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64//thin, layers[0])
        self.layer2 = self._make_layer(block, 128//thin, layers[1], stride=2,
                                       dilate=replace_stride_with_dilation[0])
        self.layer3 = self._make_layer(block, 256//thin, layers[2], stride=2,
                                       dilate=replace_stride_with_dilation[1])
        self.layer4 = self._make_layer(block, 512//thin, layers[3], stride=2,
                                       dilate=replace_stride_with_dilation[2])
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512//thin * block.expansion, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
        downsample = None
        previous_dilation = self.dilation
        if dilate:
            self.dilation *= stride
            stride = 1
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                conv1x1(self.inplanes, planes * block.expansion, stride))

        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
                            self.base_width, previous_dilation))
        self.inplanes = planes * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.inplanes, planes, groups=self.groups,
                                base_width=self.base_width, dilation=self.dilation))
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

    # Returns gradient norm for each layer of the model.
    def gradient_norms(self):
        all_norms = []
        for l in [self.conv1, self.layer1, self.layer2, self.layer3, self.layer4, self.fc]:
            layer_norm = 0
            for p in l.parameters():
                if p.grad is not None:
                    pnorm = p.grad.data.norm(2)
                    layer_norm += pnorm.item()**2
            layer_norm = layer_norm**(0.5)
            all_norms.append(layer_norm)
        return all_norms

class ResNetSeg(ResNetSimple):

    def __init__(self, block, layers, out_dims, num_classes=1000,
                 groups=1, width_per_group=64, replace_stride_with_dilation=None,
                 thin=1):
        super(ResNetSeg, self).__init__(block, layers, num_classes, groups,
            width_per_group, replace_stride_with_dilation, thin)

        self.avgpool = nn.AdaptiveAvgPool2d(out_dims)
        self.conv2 = nn.Conv2d(512//thin * block.expansion, num_classes, kernel_size=1, stride=1)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def forward(self, x):
        x = self.conv1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = self.conv2(x)

        return x

def resnet18(n_classes, thin=1):
    return ResNetSimple(BasicBlock, [2, 2, 2, 2], num_classes=n_classes, thin=thin)

def resnetseg34(n_classes, out_dims, thin=1):
    return ResNetSeg(BasicBlock, [3,4,6,3], out_dims, num_classes=n_classes, thin=thin)

######################################

import time, copy
from .utils import class_auroc

def train_rnseg(model, dataloaders, criterion, optimizer, num_epochs, outfile=None):
    since = time.time()

    val_acc_history = []

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0

    # GPU support
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    model.to(device)

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1), flush=True)
        print('-' * 10, flush=True)

        # Each epoch has a training and validation phase
        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode

            running_loss = 0.0
            running_corrects = 0
            running_foreground = 0

            epoch_labels, epoch_softmax = [],[]

            # Iterate over data.
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                # zero the parameter gradients
                optimizer.zero_grad()

                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):
                    # Get model outputs, then filter for foreground patches (label>0).
                    # Use only foreground patches in loss/accuracy calulcations.
                    outputs = model(inputs)
                    outputs = outputs.permute((0,2,3,1))
                    outputs = torch.reshape(outputs, (-1, outputs.shape[-1]))
                    labels = torch.reshape(labels, (-1,))
                    outputs = outputs[labels > 0]
                    labels = labels[labels > 0]
                    labels -= 1

                    loss = criterion(outputs, labels)
                    _, preds = torch.max(outputs, 1)

                    epoch_labels.append(labels)
                    epoch_softmax.append(nn.functional.softmax(outputs, dim=1))

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # statistics
                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)
                running_foreground += len(labels)

            epoch_loss = running_loss / len(dataloaders[phase].dataset)
            epoch_acc = running_corrects.double() / running_foreground

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc), flush=True)

            epoch_labels = np.concatenate([x.cpu().data.numpy() for x in epoch_labels])
            epoch_softmax = np.concatenate([x.cpu().data.numpy() for x in epoch_softmax])
            auroc = class_auroc(epoch_softmax, epoch_labels)
            print('{} AUROC: {}'.format(phase, "\t".join(list(map(str, auroc)))))

            # deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())
                if outfile is not None:
                    torch.save(model.state_dict(), outfile)
            if phase == 'val':
                val_acc_history.append(epoch_acc)

        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60), flush=True)
    print('Best val Acc: {:4f}'.format(best_acc), flush=True)

    # load best model weights
    model.load_state_dict(best_model_wts)
    return model, val_acc_history


#!/usr/bin/env python
# -*- coding: utf-8 -*-

## file: test_db.py
## desc: Unit tests for db.py.
## auth: TR

import warnings

## Ignore binary wheel warnings from psycopg2
warnings.filterwarnings('ignore', module='psycopg2')

from gwlib import config
from gwlib import db

config.load_config('tests/test.cfg')

def test_connect():

    success, err = db.connect(
        config.get_db('host'),
        config.get_db('database'),
        config.get_db('user'),
        config.get_db('password'),
        config.get_db('port')
    )

    assert success

def test_get_species():

    res = db.get_species()

    assert res == {
        'Mus musculus': 1,
        'Homo sapiens': 2,
        'Rattus norvegicus': 3
    }

def test_get_species_with_taxid():

    res = db.get_species_with_taxid()

    assert res == [
        {'sp_id': 1, 'sp_taxid': 10090, 'sp_name': 'Mus musculus'},
        {'sp_id': 2, 'sp_taxid': 9606, 'sp_name': 'Homo sapiens'},
        {'sp_id': 3, 'sp_taxid': 10116, 'sp_name': 'Rattus norvegicus'}
    ]

def test_get_species_by_taxid():

    res = db.get_species_by_taxid()

    assert res == {
        10090: 1,
        9606: 2,
        10116: 3
    }

def test_get_attributions():

    res = db.get_attributions()

    assert res == {
        'GO': 8,
        'GWAS': 6,
        'CTD': 2
    }

#    100 INSERT INTO odestatic.genedb (gdb_id, gdb_name, gdb_shortname, sp_id)
#  1 VALUES      (2, 'Ensembl Gene', 'ensembl', 0),
#  2             (7, 'Gene Symbol', 'symbol', 0),
#  3             (10, 'MGI', 'mgi', 1),
#  4             (11, 'HGNC', 'hgnc', 2),
#  5             (12, 'RGD', 'rgd', 3),
#  6             (25, 'Variant', 'variant', 0);
#  7
#  8 INSERT INTO extsrc.gene (ode_gene_id, ode_ref_id, gdb_id, sp_id, ode_pref)
#  9 VALUES      (5105, 'MGI:108511', 10, 1, FALSE),
# 10             (5105, 'Mobp', 7, 1, TRUE),
# 11             (5105, 'ENSMUSG00000032517', 2, 1, FALSE),
# 12             (66945, 'HGNC:7189', 11, 2, FALSE),
# 13             (66945, 'MOBP', 7, 2, TRUE),
# 14             (66945, 'ENSG00000168314', 2, 2, FALSE),
# 15             (124272, 'RGD3101', 12, 3, FALSE),
# 16             (124272, 'Mobp', 7, 3, TRUE),
# 17             (124272, 'ENSRNOG00000018700', 2, 3, FALSE);

def test_get_gene_ids_1():

    res = db.get_gene_ids(['MGI:108511', 'HGNC:7189', 'ENSRNOG00000018700'])

    assert res == {
        'MGI:108511': 5105,
        'HGNC:7189': 66945,
        'ENSRNOG00000018700': 124272
    }

def test_get_gene_ids_2():

    res = db.get_gene_ids(['MGI:108511', 'HGNC:7189', 'ENSRNOG00000018700'], sp_id=1)

    assert res == {'MGI:108511': 5105}

def test_get_gene_ids_3():

    res = db.get_gene_ids(['MGI:108511', 'HGNC:7189', 'ENSRNOG00000018700'], gdb_id=11)

    assert res == {'HGNC:7189': 66945}

def test_get_gene_ids_4():

    res = db.get_gene_ids(['MOBP'], gdb_id=7, sp_id=2)

    assert res == {'MOBP': 66945}

def test_get_gene_ids_5():

    res = db.get_gene_ids(['mobp'], gdb_id=7, sp_id=2)

    assert res == {}

def test_get_species_genes():

    res = db.get_species_genes(1, symbol=False)

    assert res == {
        'MGI:108511': 5105,
        'Mobp': 5105,
        'ENSMUSG00000032517': 5105,
        'MGI:87948': 73,
        'MGI:88336': 323
    }

def test_get_gene_refs_1():

    res = db.get_gene_refs([5105])

    assert 5105 in res
    assert 'MGI:108511' in res[5105]
    assert 'Mobp' in res[5105]
    assert 'ENSMUSG00000032517' in res[5105]

def test_get_gene_refs_2():

    res = db.get_gene_refs([5105, 66945, 124272], type_id=7)

    assert 5105 in res
    assert 66945 in res
    assert 124272 in res
    assert res[5105] == ['Mobp']
    assert res[66945] == ['MOBP']
    assert res[124272] == ['Mobp']

def test_get_preferred_gene_refs():

    res = db.get_preferred_gene_refs([5105, 66945, 124272])

    assert res == {
        5105: 'Mobp',
        66945: 'MOBP',
        124272: 'Mobp'
    }

def test_get_genesets():

    res = db.get_genesets([185236])

    assert len(res) == 1
    assert res[0]['gs_id'] == 185236
    assert res[0]['gs_abbreviation'] == 'GO:0006184'

def test_get_geneset_ids_by_tier_1():

    res = db.get_geneset_ids(tiers=[1])

    assert len(res) == 2
    assert set(res) == set([185236, 270867])

def test_get_geneset_ids_by_tier_2():

    res = db.get_geneset_ids(tiers=[1, 3])

    assert len(res) == 3
    assert set(res) == set([185236, 270867, 219234])

def test_get_geneset_ids_by_attribute_1():

    res = db.get_geneset_ids(at_id=2)

    assert len(res) == 0

def test_get_geneset_ids_by_attribute_2():

    res = db.get_geneset_ids(at_id=8)

    assert len(res) == 1
    assert res == [185236]

def test_get_geneset_ids_by_attribute_3():

    res = db.get_geneset_ids(at_id=6, sp_id=2)

    assert len(res) == 1
    assert res == [270867]

def test_get_gene_homologs():

    res = db.get_gene_homologs([5105, 124272, 66945])

    assert res == {
        5105: 32040,
        124272: 32040,
        66945: 32040
    }

def test_get_publication():

    res = db.get_publication('17440432')

    assert res == 2312

def test_get_publication_pmid():

    res = db.get_publication_pmid(2312)

    assert res == '17440432'

def test_get_geneset_pmids():

    res = db.get_geneset_pmids([219234, 270867])

    assert res == {
        219234: '17440432',
        270867: '26077402'
    }

def test_get_geneset_metadata():

    res = db.get_geneset_metadata([219234, 270867])

    assert len(res) == 2
    assert 'gs_id' in res[0]
    assert 'gs_name' in res[0]
    assert 'gs_description' in res[0]
    assert 'gs_abbreviation' in res[0]

def test_get_gene_types_1():

    res = db.get_gene_types()

    assert res == {
        'Ensembl Gene': 2,
        'Gene Symbol': 7,
        'MGI': 10,
        'HGNC': 11,
        'RGD': 12,
        'Variant': 25,
    }

def test_get_gene_types_2():

    res = db.get_gene_types(short=True)

    assert res == {
        'ensembl': 2,
        'symbol': 7,
        'mgi': 10,
        'hgnc': 11,
        'rgd': 12,
        'variant': 25,
    }

def test_get_score_types():

    res = db.get_score_types()

    assert res == {
        'p-value': 1,
        'q-value': 2,
        'binary': 3,
        'correlation': 4,
        'effect': 5
    }

def test_get_threshold_types_1():

    res = db.get_threshold_types()

    assert res == dict(zip(
        ['P-value', 'Q-value', 'Binary', 'Correlation', 'Effect'],
        [1, 2, 3, 4, 5]
    ))

def test_get_threshold_types_2():

    res = db.get_threshold_types(lower=True)

    assert res == dict(zip(
        ['p-value', 'q-value', 'binary', 'correlation', 'effect'],
        [1, 2, 3, 4, 5]
    ))

def test_insert_geneset_values():

    db.insert_geneset_values([
        (1, 100, 1.2, 'Mobp', True),
        (1, 200, -1.2, 'Aplp2', True)
    ])

    values = db.get_geneset_values([1])
    values = sorted(values, key=lambda g: g['ode_gene_id'])

    assert values[0]['gs_id'] == 1
    assert values[0]['ode_gene_id'] == 100
    assert values[0]['gsv_value'] == 1.2
    assert values[1]['gs_id'] == 1
    assert values[1]['ode_gene_id'] == 200
    assert values[1]['gsv_value'] == -1.2


'''
This tool provides a method to lookup constants to easily decode, for instance,
ErrorCodes.
'''

import argparse
from pathlib import Path
from collections import Counter
from tinydb import TinyDB, where
from rich.console import Console
from rich.padding import Padding

def const_lookup(search, const_type=None):
    '''
    Read the const_db.json TinyDB to look for the given value or module name.

    Additionally if the type of the constant is known narrow down the search.
    '''

    # Get the root directory
    root_dir = Path(__file__).parent.parent.parent.absolute()

    # Open database
    db = open_database(root_dir)

    # See if the search is a constant or is a module name
    try:
        search = int(search, 0)
    except ValueError:
        pass

    # Matches dictionary
    tab_matches = {table:[] for table in db.tables()}

    # Search
    if const_type is None:
        for table_name in db.tables():
            # Match for constants
            if isinstance(search, int):
                table_matches = db.table(table_name).search(where('value') == search)
            else:
                table_matches = db.table(table_name).search(where('module') == search)
                table_matches.extend(db.table(table_name).search(where('symbol') == search))
            if len(table_matches) > 0:
                tab_matches[table_name].extend(table_matches)
    else:
        # Match for constants
        if search is not None:
            tab_matches = db.table(const_type).search(where('value') == search)
        else:
            tab_matches = db.table(const_type).search(where('module') == search)

    return tab_matches

def open_database(root_dir):
    '''
    Attempt to open the database by searching the root_dir for known locations
    '''
    # First try the build dir
    try:
        return TinyDB(
            root_dir.joinpath('builds/const_db.json'),
            access_mode='r'
        )
    except FileNotFoundError:
        pass

    # Last resort is the tools dir
    return TinyDB(
        root_dir.joinpath('src/tools/const_db.json'),
        access_mode='r'
    )

def print_matches(matches):
    '''
    Format the matches in a nice way
    '''

    console = Console(highlight=False)

    # Print events
    console.print('[bold u]Events[/bold u]')
    if len(matches['events']) == 0:
        console.print('    [i]None[/i]')
    else:
        _print_duplicate_warnings(console, matches['events'], 'Events')
        for event in matches['events']:
            _print_const(console, event)
    console.print('')

    # Print errors
    console.print('[bold u]Errors[/bold u]')
    if len(matches['errors']) == 0:
        console.print('    [i]None[/i]')
    else:
        _print_duplicate_warnings(console, matches['errors'], 'Errors')
        for error in matches['errors']:
            _print_const(console, error)
    console.print('')

    # Print datapools
    console.print('[bold u]DataPool Parameters[/bold u]')
    if len(matches['datapool']) == 0:
        console.print('    [i]None[/i]')
    else:
        _print_duplicate_warnings(console, matches['datapool'], 'DataPool')
        for dp in matches['datapool']:
            _print_const(console, dp)
    console.print('')

def _print_const(console, const):
    '''
    Print an individual constant to the given console.
    '''
    console.print(f'    [bold][cyan]{const["symbol"]}[/cyan]:[/bold] 0x{const["value"]:04X} ({const["value"]}, 0b{const["value"]:016b})')
    desc = Padding(const['description'], (0, 0, 0, 8))
    console.print(desc)

def _print_duplicate_warnings(console, const_list, const_name):
    '''
    Searches const_list for repeated values and prints a warning highlighting
    them.
    '''
    counts = dict(Counter([c['value'] for c in const_list]))
    duplicates = {const:count for (const, count) in counts.items() if count > 1}

    if len(duplicates) > 0:
        console.print(f'    [red][bold]WARNING[/bold] Multiple {const_name} parameters with the same value detected![/red]')
        console.print('            [red]This must be corrected in the header definition so there is no conflict![/red]')

    for (const, count) in duplicates.items():
        console.print(f'            [red]0x{const:04X} is duplicated {count} times')

    console.print('')

def _parse_args():
    '''
    Parse arguments for the command-line version of const_lookup
    '''

    parser = argparse.ArgumentParser(
        description='Lookup constant values in the software'
    )
    parser.add_argument(
        'const_type',
        help='The type of constant, either "error", "event", "datapool"',
        nargs='?'
    )
    parser.add_argument(
        'search',
        help='The search term to lookup. Numeric values (inlcuind hex prepended with 0x) will be interpreted as constants, strings as module names',
        nargs=1
    )

    return parser.parse_args()

if __name__ == '__main__':
    args = _parse_args()

    matches = const_lookup(args.search[0], const_type=args.const_type)

    print_matches(matches)


#!/usr/bin/python3
"""
Function that queries the Reddit API and
prints the titles of the first 10 hot posts listed
for a given subreddit
"""
import requests

def top_ten(subreddit):
    url = "https://www.reddit.com/r/{}/hot.json".format(subreddit)
    params = {
        'limit': 10
    }
    req = requests.get(
        url, headers={'User-Agent': 'Python/requests'}, params=params)

    try:
        resp = req.json()
        posts = resp.get("data", {}).get("children", None)
        if posts is None:
            print(None)
        else:
            [print(post.get("data").get("title")) for post in posts]
    except Exception:
        print(None)


from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.label import MDIcon, MDLabel
from kivy.properties import StringProperty

class MDChartTitle(MDBoxLayout):

    # region Properties
    icon = StringProperty('')
    font_style = StringProperty('Body1')
    _title_list = []
    _showing_titles = []
    # endregion

    # region __init__:
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.bind(icon=self._update, font_style=self._update)
    # endregion

    # region Public Methods:
    def add_title(self, title: str, color: list):
        if len(self._showing_titles) == len(self._title_list):
            __bl = MDBoxLayout(spacing='10dp', size_hint=(None, None), adaptive_height=True)
            __label = MDLabel(font_style=self.font_style, size_hint=(None, None), adaptive_height=True)
            __icon = MDIcon(icon=self.icon, theme_text_color="Custom", text_size=__label.text_size,
                            size_hint=(None, None), adaptive_height=True)
            __bl.add_widget(__icon)
            __bl.add_widget(__label)
            self._title_list.append(__bl)
        else:
            __bl = self._title_list[len(self._showing_titles)]
            __label = __bl.children[0]
            __icon = __bl.children[1]
        __label.text = title
        __icon.text_color = color
        self.add_widget(__bl)
        self._showing_titles.append(__bl)

    def clear_titles(self):
        for idx in range(len(self._showing_titles))[::-1]:
            self.remove_widget(self._showing_titles.pop(idx))
    # endregion

    # region Bind Methods:
    def _update(self, *args):
        if self._title_list:
            for title in self._title_list:
                title.children[0].font_style = self.font_style
                title.children[1].text_size = title.children[0].text_size
                title.children[1].icon = self.icon
    # endregion


import telnetlib
import sys,os
import time
import re
from statistics import mean , stdev
import xlsxwriter
from openpyxl import load_workbook
import math
import shutil
from numpy import number
import pandas as pd
#import dill
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import datetime as dt
# Python code to illustrate
# inserting data in MongoDB
#from pymongo import MongoClient

import sys
from cmath import nan

class DataAnalyzer:
        # Initializer / Instance Attributes
    def __init__(self,filepath, name):
        self.filepath = filepath
        self.name = name

    def listFiles(self):
        fileArray = []

        files = os.listdir(self.filepath)
        for name in files:
            fileArray.append(name)

        return fileArray

    def getAbsolutePath(self):
        absFile=self.filepath +'/'+self.name
        return absFile

    def readWeather(self):
        file = open(self.getAbsolutePath(), "r")
        fileStr = file.read()
        numarray=[]
        timearray=[]
        count=0
        for string in fileStr.splitlines():
            #20190730-09:42:40.4

            timeval = string.split(',')[0].replace("Weather:","")
            #InTemp:86.0,InHum:49.0,OutTemp:75.5,WindS:6.0,WindD:355.0,OutHum:83.0,UV:0.0,SolRad:0.0
            intemp = str(round(float(string.split(',')[1].replace("InTemp:","")),2))
            inhum = str(round(float(string.split(',')[2].replace("InHum:","")),2))
            outtemp = str(round(float(string.split(',')[3].replace("OutTemp:","")),2))
            winds = str(round(float(string.split(',')[4].replace("WindS:","")),2))
            windd = str(round(float(string.split(',')[5].replace("WindD:","")),2))
            outhum = str(round(float(string.split(',')[6].replace("OutHum:","")),2))
            uv = str(round(float(string.split(',')[7].replace("UV:","")),2))
            solrad = str(round(float(string.split(',')[8].replace("SolRad:","")),2))
            print(self.round_time(timeval)+','+intemp+','+inhum+','+outtemp+','+winds+','+windd+','+outhum+','+uv+','+solrad)
            """numarray.append(round(float(number),2))
            timearray.append(self.round_time(timeval))"""

            count+=1

        return numarray, timearray

    def round_time(self,key):
        minute = int(key.split(":")[-2])
        date = int(key.split(":")[0].split("-")[0])
        hour = int(key.split(":")[0].split("-")[1])

        seconds = round(float(key.split(":")[-1])*2)/2.0
        if seconds>=60.0:
            minute = minute +1
            seconds = 0.0

        if minute>=60.0:
            hour = hour+1
            minute= 0.0

        if hour >=24:
            date = date +1
            if date==20190931:
                date=20191001
            elif date==20190832:
                date=20190901
            elif date==20190732:
                date=20190801
            hour=0.0

        if seconds < 10.0:
            tmp = str('%02d' % int(str(seconds).split(".")[0])) + '.'+str(seconds).split(".")[1]
            seconds =tmp

        x=str(date)
        date_str_new= x[0]+x[1]+x[2]+x[3]+"-"+x[4]+x[5]+"-"+x[6]+x[7]
        #objDate = datetime.strptime(, '%y%m%d')
        final_datetime = str(date_str_new)+" " +str('%02d' % hour)+":"+str('%02d' % minute)+":"+str(seconds)
        #datetime_object_final = datetime.strptime(final_datetime, '%Y-%m-%d %H:%M:%S.%f')
        return final_datetime

    def printFile(self):
        file = open(self.getAbsolutePath(), "r")
        fileStr = file.read()
        for i in fileStr.splitlines():
            print(i)

    def getFileStr(self):
        file = open(self.getAbsolutePath(), "r")
        fileStr = file.read()
        return fileStr

    def readFile(self):
        file = open(self.getAbsolutePath(), "r")
        fileStr = file.read()
        numarray=[]
        timearray=[]
        count=0
        for string in fileStr.splitlines():
            #20190730-09:42:40.4
            if "/sec" in string:
                try:
                    number  = string.split(',')[1].split(' ')[0]
                    tmpval  = string.split(',')[1].split(' ')[1]
                    timeval = string.split(',')[0].split(' ')[1]
                    if tmpval == "Mbits/sec":
                        numarray.append(round(float(number),2))
                        timearray.append(self.round_time(timeval))

                    elif tmpval == "Kbits/sec":
                        numarray.append(round(float(number)*0.001,2))
                        timearray.append(self.round_time(timeval))
                    elif tmpval == "bits/sec":
                        numarray.append(round(float(number)*0.001,2))
                        timearray.append(self.round_time(timeval))
                except:
                    print(str(count)+" - EXCEPTION")
            count+=1

        return numarray, timearray
"""def pushToDatabaset(rf):
    conn = MongoClient('localhost',27017)
    # database
    db = conn.example
    # Created or Switched to collection names: my_gfg_collection
    collection = db.throughput_collection_fso
    #collection_fso = db.throuhgput_collection_fso
    for i in rf.splitlines():
        time = i.split(",")[0]
        throughput = i.split(",")[1]
        dbobject = {
                "time": time,
                "throughput":throughput
                }
        rec_id1 = collection.insert_one(dbobject)
        print("Data inserted with record ids",rec_id1)
"""
def compare_time_throughput(time_rf,time_fso,throughput_rf,throughput_fso):
    i=0
    time_ind_for_fso=[]
    time_ind_for_rf=[]
    tmp=0
    total_array = []
    while i < len(time_rf):

        throughput = []
        try:
            if time_rf[i] in time_fso:
                condition =True
                print(time_rf[i], end =",")
                print(throughput_rf[i], end =",")
                #print("RF: " + time_rf[i])
                while condition:
                    if time_rf[i] != time_fso[tmp]:
                        tmp+=1
                    else:
                        #print("FSO: " + time_fso[tmp])
                        print(throughput_fso[i])
                        condition = False

            else:
                print(str(i) + " - SKIPPED")
                #time_ind_for_fso.append(1)
                #time_ind_for_rf.append(time_fso.index(i))
        except:
            print("Nope")
        total_array.append(throughput)
        i += 1

    return total_array

def checkDuplicate(rf):
    oldtime=""
    for i in rf:
        if oldtime != i.split(',')[0]:
            print(i.split(',')[0]+','+i.split(',')[1])
            oldtime = i.split(',')[0]
        else:
            print("Hop")

def printFso(timeval, rf, fso):
    r_ind=0
    f_ind=0
    cnt_rf = 0
    cnt_fso=0
    for time_tmp in timeval:
        time_str=time_tmp
        tptval_rf =""
        tptval_fso =""
        if time_tmp == rf[r_ind].split(',')[0]:
            tptval_rf = rf[r_ind].split(',')[1]
            time_str = time_str + ',' + rf[r_ind].split(',')[1]
            r_ind+=1
        else:
            time_str = time_str + ',NORF'

        if time_tmp == fso[f_ind].split(',')[0]:
            tptval_fso = fso[f_ind].split(',')[1]
            time_str = time_str + ',' + fso[f_ind].split(',')[1]
            f_ind+=1
        else:
            time_str = time_str + ',NOFSO'
        #print(time_str)

        if tptval_rf != "" and tptval_fso != "":
            if float(tptval_rf) >= float(tptval_fso):
                cnt_rf +=1
                print("RF:"+str(cnt_rf))
            else:
                cnt_fso +=1
                print("FSO:"+str(cnt_fso))

    print(cnt_rf)
    print(cnt_fso)

def printWeather(timeval, weather):
    r_ind=0

    for time_tmp in timeval:
        time_str=time_tmp
        tptval_rf =""
        if time_tmp == weather[r_ind].split(',')[0]:
            tptval_rf = ','.join(weather[r_ind].split(',')[1:])
            time_str = time_str + ','+tptval_rf
            r_ind+=1
        else:
            time_str = time_str + ',NOWEATHER'

        print(time_str)

def printWeatherline(weather):
    count=1
    for time_tmp in weather:
        if 'NOWEATHER' in time_tmp:
            #if weather[count-1].split(',')[0] == time_tmp.split(',')[0]:
            print(time_tmp.split(',')[0]+','+','.join(weather[count-2].split(',')[1:]))
        else:
            print(time_tmp)
        count+=1

def printall(timeval, weather):
    count=0
    for time_tmp in timeval:
        print(time_tmp +","+','.join(weather[count].split(',')[1:]))
        count+=1

def printKeyInterval(timeval, key):
    count=0
    samet=0
    keyval_array=[]
    exit_flag=True
    tmp=0
    old_flag = True
    for time_tmp in timeval:
        if key in time_tmp:
            keyval_array.append(count)

        count+=1

    print(keyval_array)

#printWeatherline(weather_data)
#rfthroughput.printFile()
def getNoneElementsOneArray(fso):
    counter=0
    none_counter=0
    index_list = []
    last_remaining_index_old = 0
    while counter < len(fso):
        if fso[counter]==None:

            none_counter +=1
            if none_counter ==1:
                last_remaining_index_old = counter
        else:
            if none_counter >1:
                index_list.append([last_remaining_index_old,none_counter])
            none_counter=0
        counter+=1
    return index_list

def getNoneElementsTwoArray(fso,rf):
    counter=0
    none_counter=0
    index_list = []
    last_remaining_index_old = 0
    while counter < len(fso):
        if fso[counter]==None and rf[counter]==None:

            none_counter +=1
            if none_counter ==1:
                last_remaining_index_old = counter
        else:
            if none_counter >1:
                index_list.append([last_remaining_index_old,none_counter])
            none_counter=0
        counter+=1
    return index_list

def printPrevious(noneElements,main_array):

    out_array = main_array
    timearray=[]
    count = 0
    index_end=0
    index_count=0
    k=0
    i=0
    while i <len(main_array):

        if k <len(noneElements):
            index_end = noneElements[k][0]
            index_count = noneElements[k][1]

        timeval = main_array[i].split(',')[0]
        copy_item = ','.join(main_array[i].split(',')[1:])
        if i < index_end:
        #timearray.append(timeval+','+copy_item)
            print(timeval+','+copy_item)
            i+=1

        else:
            #print(timeval+','+copy_item)
            tmp_count = index_count
            while tmp_count >1:
                print(main_array[i].split(',')[0] +','+ ','.join(main_array[index_end-tmp_count].split(',')[1:]) )
                tmp_count-=1
                i+=1

            k+=1

def compareThroughput(throughput_rf,throughput_fso):
    i=0
    time_ind_for_fso=[]
    time_ind_for_rf=[]
    tmp=0
    count=0
    total_array = []
    while i < len(throughput_rf):
        try:
            if throughput_rf[i] > throughput_fso[i]:
                count+=1
        except:
            print("None occured - " +str(i))
        i+=1
    return count/len(throughput_rf)

def printWeatherNone(timearray,rf,fso,weather):
    count=0
    repeat_count=0
    old_count=0
    for i in weather:
        if i == "NOWEATHER":
            if repeat_count==0:
                old_count = count
            repeat_count = 1
        else:
            repeat_count = 0

        if repeat_count > 0:
            print(timearray[count]+','+str(rf[count])+','+str(fso[count])+','+weather[old_count-1])
        else:
            print(timearray[count]+','+str(rf[count])+','+str(fso[count])+','+i )
        count +=1

def printRfNone(timearray,rf,fso,weather):
    count=0
    repeat_count=0
    old_count=0
    for i in rf:
        if i == None:
            if repeat_count==0:
                old_count = count
            repeat_count = 1
        else:
            repeat_count = 0

        if repeat_count > 0:
            print(timearray[count]+','+str(rf[old_count-1])+','+str(fso[count])+','+weather[count])
        else:
            print(timearray[count]+','+str(i)+','+str(fso[count])+','+weather[count] )
        count +=1

def printFsoNone(timearray,rf,fso,weather):
    count=0
    repeat_count=0
    old_count=0
    for i in fso:
        if i == None:
            if repeat_count==0:
                old_count = count
            repeat_count = 1
        else:
            repeat_count = 0

        if repeat_count > 0:
            print(timearray[count]+','+str(rf[count])+','+str(fso[old_count-1])+','+weather[count])
        else:
            print(timearray[count]+','+str(rf[count])+','+str(i)+','+weather[count] )
        count +=1
def removePast(old,new,timeval,rf,fso,weather):
    c=0
    tmp_count=new-old
    while c < len(timeval):
        if c < old:
            print(timeval[c])
        elif old <=c <= new:
            print(timearray[c]+','+str(rf[old-tmp_count])+','+str(fso[old-tmp_count])+','+weather[old-tmp_count])
            tmp_count-=1
        elif c > new:
            print(timeval[c])
        c+=1

def getWeekAverage(array,number):
    if number == 31:
        return np.average(array[0:966878])
    elif number == 32:
        return np.average(array[966879:2176478])
    elif number == 33:
        return np.average(array[2176479:3386078])
    elif number == 34:
        return np.average(array[3386079:4595678])
    elif number == 35:
        return np.average(array[4595679:5805278])
    elif number == 36:
        return np.average(array[5805279:7014878])
    elif number == 37:
        return np.average(array[7014879:8224478])
    elif number == 38:
        return np.average(array[8224479:9434078])
    elif number == 39:
        return np.average(array[9434079:10643678])
    elif number == 40:
        return np.average(array[10643679:11853278])
    elif number == 41:
        return np.average(array[11853279:13062878])
    elif number == 42:
        return np.average(array[13062879:14272478])
    elif number == 43:
        return np.average(array[14272479:-1])

def getDuration(array):

    i = 1
    switch_count=0
    duration=0
    switch_duration=[]
    while i< len(array):
        if array[i]==1 and array[i-1] == 0:
            duration=0.5
        elif array[i]==1 and array[i-1] == 1:
            duration+=0.5
        elif array[i]==0 and array[i-1] == 0:
            if duration > 0.0:
                switch_duration.append(duration)
            duration=0.0
        i+=1

def getWeekSwitchCount(array,number):
    if number == 31:
        switch_array = array[0:966878]
    elif number == 32:
        switch_array = array[966879:2176478]
    elif number == 33:
        switch_array = array[2176479:3386078]
    elif number == 34:
        switch_array = array[3386079:4595678]
    elif number == 35:
        switch_array = array[4595679:5805278]
    elif number == 36:
        switch_array = array[5805279:7014878]
    elif number == 37:
        switch_array = array[7014879:8224478]
    elif number == 38:
        switch_array = array[8224479:9434078]
    elif number == 39:
        switch_array = array[9434079:10643678]
    elif number == 40:
        switch_array = array[10643679:11853278]
    elif number == 41:
        switch_array = array[11853279:13062878]
    elif number == 42:
        switch_array = array[13062879:14272478]
    elif number == 43:
        switch_array = array[14272479:-1]

x = [1,1,1,0,0,1,0,0,0,1,0]
switch_dur, switch_count = getDuration(x)
filename="."
#sys.stdout=open("E:\\PhD\\filtering_2.txt","w")
time_str = DataAnalyzer(filename,"latest_dataset_rf_fso_20191219.txt")
timeval = time_str.getFileStr().splitlines()
####### TAKING THE VARIABLES INTO ARRAYS#######
timearray=[]
rf=[]
fso=[]
weather=[]
for i in timeval:
    timearray.append(i.split(',')[0])
    try:
        rf.append(float(i.split(',')[1]))
    except:
        rf.append(None)

    try:
        if float(i.split(',')[2])>99.0:
            fso.append(94.4)
        else:
            fso.append(float(i.split(',')[2]))
    except:
        fso.append(None)

    #weather.append(','.join(i.split(',')[3:]))

count_rf = 0
count_fso= 0
i=0
hard_rf_fso=[]
switching=[]
while i < len(rf):
    if rf[i] >= fso[i]:
        count_rf +=1
        hard_rf_fso.append(rf[i])
        switching.append(1)
    else:
        count_fso +=1
        hard_rf_fso.append(fso[i])
        switching.append(0)
    i+=1

"""rf_31 = getWeekAverage(rf, 31)
rf_32 = getWeekAverage(rf, 32)
rf_33 = getWeekAverage(rf, 33)
rf_34 = getWeekAverage(rf, 34)
rf_35 = getWeekAverage(rf, 35)
rf_36 = getWeekAverage(rf, 36)
rf_37 = getWeekAverage(rf, 37)
rf_38 = getWeekAverage(rf, 38)
rf_39 = getWeekAverage(rf, 39)
rf_40 = getWeekAverage(rf, 40)
rf_41 = getWeekAverage(rf, 41)
rf_42 = getWeekAverage(rf, 42)
rf_43 = getWeekAverage(rf, 43)

fso_31 = getWeekAverage(fso, 31)
fso_32 = getWeekAverage(fso, 32)
fso_33 = getWeekAverage(fso, 33)
fso_34 = getWeekAverage(fso, 34)
fso_35 = getWeekAverage(fso, 35)
fso_36 = getWeekAverage(fso, 36)
fso_37 = getWeekAverage(fso, 37)
fso_38 = getWeekAverage(fso, 38)
fso_39 = getWeekAverage(fso, 39)
fso_40 = getWeekAverage(fso, 40)
fso_41 = getWeekAverage(fso, 41)
fso_42 = getWeekAverage(fso, 42)
fso_43 = getWeekAverage(fso, 43)

rffso_31 = getWeekAverage(hard_rf_fso, 31)
rffso_32 = getWeekAverage(hard_rf_fso, 32)
rffso_33 = getWeekAverage(hard_rf_fso, 33)
rffso_34 = getWeekAverage(hard_rf_fso, 34)
rffso_35 = getWeekAverage(hard_rf_fso, 35)
rffso_36 = getWeekAverage(hard_rf_fso, 36)
rffso_37 = getWeekAverage(hard_rf_fso, 37)
rffso_38 = getWeekAverage(hard_rf_fso, 38)
rffso_39 = getWeekAverage(hard_rf_fso, 39)
rffso_40 = getWeekAverage(hard_rf_fso, 40)
rffso_41 = getWeekAverage(hard_rf_fso, 41)
rffso_42 = getWeekAverage(hard_rf_fso, 42)
rffso_43 = getWeekAverage(hard_rf_fso, 43)

print("31- RF: " + str(rf_31) + "Mbps , FSO: " + str(fso_31) + "Mbps, RF-FSO: " + str(rffso_31) +"Mbps")
print("32- RF: " + str(rf_32) + "Mbps , FSO: " + str(fso_32) + "Mbps, RF-FSO: " + str(rffso_32) +"Mbps")
print("33- RF: " + str(rf_33) + "Mbps , FSO: " + str(fso_33) + "Mbps, RF-FSO: " + str(rffso_33) +"Mbps")
print("34- RF: " + str(rf_34) + "Mbps , FSO: " + str(fso_34) + "Mbps, RF-FSO: " + str(rffso_34) +"Mbps")
print("35- RF: " + str(rf_35) + "Mbps , FSO: " + str(fso_35) + "Mbps, RF-FSO: " + str(rffso_35) +"Mbps")
print("36- RF: " + str(rf_36) + "Mbps , FSO: " + str(fso_36) + "Mbps, RF-FSO: " + str(rffso_36) +"Mbps")
print("37- RF: " + str(rf_37) + "Mbps , FSO: " + str(fso_37) + "Mbps, RF-FSO: " + str(rffso_37) +"Mbps")
print("38- RF: " + str(rf_38) + "Mbps , FSO: " + str(fso_38) + "Mbps, RF-FSO: " + str(rffso_38) +"Mbps")
print("39- RF: " + str(rf_39) + "Mbps , FSO: " + str(fso_39) + "Mbps, RF-FSO: " + str(rffso_39) +"Mbps")
print("40- RF: " + str(rf_40) + "Mbps , FSO: " + str(fso_40) + "Mbps, RF-FSO: " + str(rffso_40) +"Mbps")
print("41- RF: " + str(rf_41) + "Mbps , FSO: " + str(fso_41) + "Mbps, RF-FSO: " + str(rffso_41) +"Mbps")
print("42- RF: " + str(rf_42) + "Mbps , FSO: " + str(fso_42) + "Mbps, RF-FSO: " + str(rffso_42) +"Mbps")
print("43- RF: " + str(rf_43) + "Mbps , FSO: " + str(fso_43) + "Mbps, RF-FSO: " + str(rffso_43) +"Mbps")
"""

"""
31- RF: 32.123Mbps , FSO: 85.861Mbps, RF-FSO: 87.206Mbps
32- RF: 33.127Mbps , FSO: 85.713Mbps, RF-FSO: 86.602Mbps
33- RF: 34.352Mbps , FSO: 86.576Mbps, RF-FSO: 87.830Mbps
34- RF: 40.232Mbps , FSO: 83.781Mbps, RF-FSO: 87.140Mbps
35- RF: 41.699Mbps , FSO: 76.555Mbps, RF-FSO: 81.003Mbps
36- RF: 41.740Mbps , FSO: 78.845Mbps, RF-FSO: 82.418Mbps
37- RF: 40.638Mbps , FSO: 82.372Mbps, RF-FSO: 84.690Mbps
38- RF: 36.752Mbps , FSO: 85.669Mbps, RF-FSO: 87.956Mbps
39- RF: 37.007Mbps , FSO: 90.152Mbps, RF-FSO: 90.962Mbps
40- RF: 37.389Mbps , FSO: 83.195Mbps, RF-FSO: 86.241Mbps
41- RF: 35.434Mbps , FSO: 89.823Mbps, RF-FSO: 91.193Mbps
42- RF: 33.507Mbps , FSO: 89.699Mbps, RF-FSO: 91.133Mbps
43- RF: 38.751Mbps , FSO: 90.152Mbps, RF-FSO: 90.634Mbps
"""


import sys
sys.setrecursionlimit(10**6)
input = sys.stdin.readline

def dfs(x, y):
    global cnt
    for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
        nx, ny = x + dx, y + dy
        if 0 <= nx < N and 0 <= ny < M:
            if not cheese[nx][ny] and not visited[nx][ny]: # 공기
                visited[nx][ny] = 1
                dfs(nx, ny)
            elif cheese[nx][ny] and not visited[nx][ny]: # 한 변에 접촉한 치즈
                visited[nx][ny] = 1
            elif cheese[nx][ny] and visited[nx][ny]: # 두 변에 접촉한 치즈
                cheese[nx][ny] = 0 # 녹음
                cnt += 1 # 녹인 개수 누적

N, M = map(int, input().split())
cheese = [list(map(int, input().split())) for _ in range(N)]

time = 0
while True:
    visited = [[0] * M for _ in range(N)]
    cnt = 0
    dfs(0, 0)
    if not cnt:
        break
    time += 1

print(time)


# https://leetcode.com/problems/maximum-value-of-k-coins-from-piles/

# There are n piles of coins on a table. Each pile consists of a positive number of coins of assorted denominations.
# In one move, you can choose any coin on top of any pile, remove it, and add it to your wallet.
# Given a list piles, where piles[i] is a list of integers denoting the composition of the ith pile from top to bottom,
# and a positive integer k, return the maximum total value of coins you can have in your wallet if
# you choose exactly k coins optimally.

# Example 1:
# Input: piles = [[1,100,3],[7,8,9]], k = 2
# Output: 101
# Explanation:
# The above diagram shows the different ways we can choose k coins.
# The maximum total we can obtain is 101.

# Example 2:
# Input: piles = [[100],[100],[100],[100],[100],[100],[1,1,1,1,1,1,700]], k = 7
# Output: 706
# Explanation:
# The maximum total can be obtained if we choose all coins from the last pile.

# --------------- Runtime 5155 ms, beats 36.48%. Memory 38.5MB, beats 77.36% ---------------
from typing import List

class Solution:
    def maxValueOfCoins(self, piles: List[List[int]], k: int) -> int:
        length = len(piles)
        dp = [[-1] * (k + 1) for _ in range(length)]

        def search(pile, coins):
            if pile == length:
                return 0
            if dp[pile][coins] != -1:
                return dp[pile][coins]

            dp[pile][coins] = search(pile + 1, coins)
            curr_pile = 0

            for i in range(min(len(piles[pile]), coins)):
                curr_pile += piles[pile][i]
                dp[pile][coins] = max(
                    dp[pile][coins], curr_pile + search(pile + 1, coins - i - 1)
                )

            return dp[pile][coins]

        return search(0, k)


import os
import json
import boto3

# Initialize the S3 client
s3_client = boto3.client('s3')

# Read the EXPIRES_IN value from environment variables; default to 3600 seconds (1 hour) if not set
EXPIRES_IN = int(os.environ.get('EXPIRES_IN', 3600))

# Function to generate a presigned URL for uploading a file to S3
def generate_presigned_upload_url(bucket, folder, file_name):
    try:
        key = f"{folder}/{file_name}"
        presigned_url = s3_client.generate_presigned_url(
            'put_object',
            Params={
                'Bucket': bucket,
                'Key': key
            },
            ExpiresIn=EXPIRES_IN
        )
        return presigned_url, None
    except Exception as e:
        return None, str(e)

# The main Lambda function handler
def lambda_handler(event, context):
    try:
        # Parse the 'body' from the event payload and load it as JSON
        body = json.loads(event['body'])

        # Extract the 'files' list from the parsed JSON body
        files = body.get('files', [])

        response = []

        for file in files:
            bucket = file['bucket']
            folder = file['folder']
            file_name = file['file_name']

            presigned_url, error = generate_presigned_upload_url(bucket, folder, file_name)

            if presigned_url:
                response.append({
                    'bucket': bucket,
                    'folder': folder,
                    'file_name': file_name,
                    'presigned_url': presigned_url
                })
            else:
                response.append({
                    'bucket': bucket,
                    'folder': folder,
                    'file_name': file_name,
                    'error': error
                })

        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json'
            },
            'body': json.dumps(response)
        }

    except Exception as e:
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json'
            },
            'body': json.dumps({'error': str(e)})
        }


import codecs

if __name__ == '__main__':
    dp = []
    with codecs.open('p081_matrix.txt', 'r', encoding='utf-8') as fin:
        for idx, line in enumerate(fin):
            line = line.strip().split(',')
            tem = []
            for jdx, item in enumerate(line):
                item = int(item)
                if idx == 0 and jdx == 0:
                    tem.append(item)
                elif idx == 0:
                    tem.append(item + tem[-1])
                elif jdx == 0:
                    tem.append(item + dp[-1][0])
                else:
                    tem.append(min(dp[-1][jdx], tem[-1]) + item)
            dp.append(tem)
        print(dp[-1][-1])


import argparse
import gzip
import shutil
import sys

# Authors: Ben Parks
# Last updated: 11/21/22

# Add mtx header to a tab-delimited file of (row, col, value) entries

# Usage: python mtx_merge.py input.mtx features.tsv.gz barcodes.tsv.gz > output.mtx
#  python mtx_merge.py counts.tsv.gz features.tsv --mtx matrix.mtx --barcodes barcodes.tv
# Inputs:
# - mtx: (row, col, value) tab-separated mtx entries (no header)
# - features: TSV of gene IDs, where gene_id is the first column
# - barcodes: TSV of cell barcodes, where cell_id is the first column
# Outputs:
# - mtx: TSV of (row, col, count) written to stdout with appropriate Matrix-Market header

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("mtx", type=str, help="Path to mtx.gz file")
    parser.add_argument("features", type=str, help="Path features.tsv file")
    parser.add_argument("barcodes", type=str, help="Path barcodes.tsv file")
    args = parser.parse_args()

    feature_count = sum(1 for line in gzip.open(args.features, "rb"))
    barcode_count = sum(1 for line in gzip.open(args.barcodes, "rb"))

    # Slow step
    nonzero_count = sum(1 for line in gzip.open(args.mtx, "rb"))

    # Write header
    sys.stdout.buffer.write(b"%%MatrixMarket matrix coordinate real general\n")
    sys.stdout.buffer.write(b"%\n")
    sys.stdout.buffer.write(f"{feature_count}\t{barcode_count}\t{nonzero_count}\n".encode())

    # Write contents
    shutil.copyfileobj(gzip.open(args.mtx, "rb"), sys.stdout.buffer)

if __name__ == "__main__":
    main()


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Remove advertising from subtitle files

This script is intended to scan subtitle files (or folders containing subtitle
files) and prompt to remove cells with advertising. Subtitle files may be
searched via regular expression or text matches. The script can handle srt
subtitle files natively or other formats (ass, srt, ssa, sub) via the aeidon
Python package.
"""

import logging
import os
import re
import sys

__program__ = 'subnuker'
__version__ = '0.4.5'

LOGGER = logging.getLogger(__program__)

class Config:   # pylint: disable=R0903

    """Store global script configuration values."""

    CHARFIXES = {'¶': '♪'}

    REGEX = ['1x', '2x', '3x', '4x', '5x', '6x', '7x', '8x', '9x',
             r'\bhttps?://', r'(?<!\.)\.(co\.uk|com|net|org)',
             'air date', 'Art Subs', 'caption', 'download', 'Hawkeye147',
             r'[Nn]anban', 'subtitle', 'sync', 'TVShow',
             r'(?<![A-Za-z0-9])www\.', 'âª', r'^♪$', r'^\*\*$']

    TERMS = ['1x', '2x', '3x', '4x', '5x', '6x', '7x', '8x', '9x',
             'http://', 'https://', '.co.uk', '.com', '.net', '.org',
             'air date', 'Art Subs', 'caption', 'download', 'Hawkeye147',
             'Nanban', 'nanban', 'subtitle', 'sync', 'TVShow',
             'www.', 'âª']

    # bool switch
    results = False

    # store parsed options/arguments
    arguments = None
    options = None

    # store terms or compiled regex
    patterns = []

class AeidonProject:

    """Process individual subtitle files with python3-aeidon."""

    def __init__(self, filename):
        try:
            import aeidon
        except ImportError:
            prerequisites()
            sys.exit(1)

        self.fix = Config.options.fix
        self.filename = filename
        self.project = aeidon.Project()  # pylint: disable=W0201

        self.open()

        self.fixchars()

        if not self.fix:
            matches = self.search()

            if matches:
                Config.results = True
                deletions = self.prompt(matches)
                if deletions:
                    self.project.remove_subtitles(deletions)

        if self.modified:
            self.save()
        elif self.fix:
            LOGGER.info("No changes were made to '%s'", self.filename)

    def fixchars(self):
        """Replace characters or strings within subtitle file."""
        for key in Config.CHARFIXES:
            self.project.set_search_string(key)
            self.project.set_search_replacement(Config.CHARFIXES[key])
            self.project.replace_all()

    @property
    def modified(self):
        """Check whether subtitle file has been modified."""
        return self.project.main_changed > 0

    def open(self):
        """Open the subtitle file into an Aeidon project."""
        try:
            self.project.open_main(self.filename)
        except UnicodeDecodeError:
            with open(self.filename, 'rb') as openfile:
                encoding = get_encoding(openfile.read())

            try:
                self.project.open_main(self.filename, encoding)
            except UnicodeDecodeError:
                LOGGER.error("'%s' encountered a fatal encoding error",
                             self.filename)
                sys.exit(1)
            except:  # pylint: disable=W0702
                open_error(self.filename)

        except:  # pylint: disable=W0702
            open_error(self.filename)

    def prompt(self, matches):
        """Prompt user to remove cells from subtitle file."""

        if Config.options.autoyes:
            return matches

        deletions = []

        for match in matches:
            os.system('clear')
            print(self.project.subtitles[match].main_text)
            print('----------------------------------------')
            print("Delete cell %s of '%s'?" % (str(match + 1), self.filename))
            response = getch().lower()
            if response == 'y':
                os.system('clear')
                deletions.append(match)
            elif response == 'n':
                os.system('clear')
            else:
                if deletions or self.modified:
                    LOGGER.warning("Not saving changes made to '%s'",
                                   self.filename)
                sys.exit(0)

        return deletions

    def save(self):
        """Save subtitle file."""
        try:
            # ensure file is encoded properly while saving
            self.project.main_file.encoding = 'utf_8'
            self.project.save_main()
            if self.fix:
                LOGGER.info("Saved changes to '%s'", self.filename)
        except:  # pylint: disable=W0702
            LOGGER.error("Unable to save '%s'", self.filename)
            sys.exit(1)

    def search(self):
        """Search srt in project for cells matching list of terms."""

        matches = []
        for pattern in Config.patterns:
            matches += self.termfinder(pattern)

        return sorted(set(matches), key=int)

    def termfinder(self, pattern):
        """Search srt in project for cells matching term."""

        if Config.options.regex:
            flags = re.M | re.S | \
                (0 if Config.options.case_sensitive else re.I)
            self.project.set_search_regex(
                pattern, flags=flags)
        else:
            self.project.set_search_string(
                pattern, ignore_case=not Config.options.case_sensitive)

        matches = []

        while True:
            try:
                if matches:
                    last = matches[-1]
                    new = self.project.find_next(last + 1)[0]
                    if new != last and new > last:
                        matches.append(new)
                    else:
                        break
                else:
                    matches.append(self.project.find_next()[0])
            except StopIteration:
                break

        return matches

class SrtProject:

    """Process individual srt files."""

    def __init__(self, filename):
        self.filename = filename
        self.modified = False

        text = self.open()

        if Config.CHARFIXES:
            text = self.fixchars(text)

        self.cells = self.split(text)

        matches = self.search()

        if matches:
            Config.results = True
            deletions = self.prompt(matches)
            if deletions:
                self.cells = remove_elements(self.cells, deletions)
                self.modified = True

        if self.modified:
            try:
                self.save()
            except:
                LOGGER.error("Failed to save '%s'\nConsider running '--fix' "
                             "or use the '--aeidon' option.", self.filename)

    def fixchars(self, text):
        """Find and replace problematic characters."""
        keys = ''.join(Config.CHARFIXES.keys())
        values = ''.join(Config.CHARFIXES.values())
        fixed = text.translate(str.maketrans(keys, values))
        if fixed != text:
            self.modified = True
        return fixed

    def open(self):
        """Open the subtitle file (detect encoding if necessary)."""
        with open(self.filename, 'rb') as file_open:
            binary = file_open.read()

        try:
            return binary.decode()
        except UnicodeDecodeError:
            encoding = get_encoding(binary)
            try:
                return binary.decode(encoding)
            except LookupError:
                return binary.decode(errors='ignore')
            except:  # pylint: disable=W0702
                open_error(self.filename)
        except:  # pylint: disable=W0702
            open_error(self.filename)

    def prompt(self, matches):
        """Prompt user to remove cells from subtitle file."""

        if Config.options.autoyes:
            return matches

        deletions = []

        for match in matches:
            os.system('clear')
            print(self.cells[match])
            print('----------------------------------------')
            print("Delete cell %s of '%s'?" % (str(match + 1), self.filename))
            response = getch().lower()
            if response == 'y':
                os.system('clear')
                deletions.append(match)
            elif response == 'n':
                os.system('clear')
            else:
                if deletions or self.modified:
                    LOGGER.warning("Not saving changes made to '%s'",
                                   self.filename)
                sys.exit(0)

        return deletions

    def renumber(self):
        """Re-number cells."""

        num = 0
        for cell in self.cells:
            cell_split = cell.splitlines()
            if len(cell_split) >= 2:
                num += 1
                cell_split[0] = str(num)
                yield '\n'.join(cell_split)

    def save(self):
        """Format and save cells."""

        # re-number cells
        self.cells = list(self.renumber())

        # add a newline to the last line if necessary
        if not self.cells[-1].endswith('\n'):
            self.cells[-1] += '\n'

        # save the rejoined the list of cells
        with open(self.filename, 'w') as file_open:
            file_open.write('\n\n'.join(self.cells))

    def search(self):
        """Return list of cells to be removed."""

        matches = []
        for index, cell in enumerate(self.cells):
            for pattern in Config.patterns:
                if ismatch(cell, pattern):
                    matches.append(index)
                    break

        return matches

    def split(self, text):
        """Split text into a list of cells."""

        import re
        if re.search('\n\n', text):
            return text.split('\n\n')
        elif re.search('\r\n\r\n', text):
            return text.split('\r\n\r\n')
        else:
            LOGGER.error("'%s' does not appear to be a 'srt' subtitle file",
                         self.filename)
            sys.exit(1)

def get_encoding(binary):
    """Return the encoding type."""

    try:
        from chardet import detect
    except ImportError:
        LOGGER.error("Please install the 'chardet' module")
        sys.exit(1)

    encoding = detect(binary).get('encoding')

    return 'iso-8859-1' if encoding == 'CP949' else encoding

def getch():
    """Request a single character input from the user."""

    if sys.platform in ['darwin', 'linux']:
        import termios
        import tty
        file_descriptor = sys.stdin.fileno()
        settings = termios.tcgetattr(file_descriptor)
        try:
            tty.setraw(file_descriptor)
            return sys.stdin.read(1)
        finally:
            termios.tcsetattr(file_descriptor, termios.TCSADRAIN, settings)
    elif sys.platform in ['cygwin', 'win32']:
        import msvcrt
        return msvcrt.getwch()

def ismatch(text, pattern):
    """Test whether text contains string or matches regex."""

    if hasattr(pattern, 'search'):
        return pattern.search(text) is not None
    else:
        return pattern in text if Config.options.case_sensitive \
            else pattern.lower() in text.lower()

def logger():
    """Configure program logger."""

    scriptlogger = logging.getLogger(__program__)

    # ensure logger is not reconfigured
    if not scriptlogger.hasHandlers():

        # set log level
        scriptlogger.setLevel(logging.INFO)

        fmt = '%(name)s:%(levelname)s: %(message)s'

        # configure terminal log
        streamhandler = logging.StreamHandler()
        streamhandler.setFormatter(logging.Formatter(fmt))
        scriptlogger.addHandler(streamhandler)

def main(args=None):
    """Start application."""

    Config.options, Config.args = parse(args)

    logger()

    if Config.options.aeidon or Config.options.fix:
        start_aeidon()
    else:
        start_srt()

    if Config.options.fix:
        sys.exit(0)

    if not Config.results:
        basenames = [os.path.basename(os.path.abspath(x)) for x in Config.args]
        print('Search of', basenames, 'returned no results.')

        # leave the terminal open long enough to read message
        if Config.options.gui:
            from time import sleep
            sleep(2)

def open_error(filename):
    """Display a generic error message upon failure to open file."""
    LOGGER.error("Unable to open '%s'", filename)
    sys.exit(1)

def parse(args):
    """Parse command-line arguments. Arguments may consist of any
    combination of directories, files, and options."""

    import argparse

    parser = argparse.ArgumentParser(
        add_help=False,
        description="Remove spam and advertising from subtitle files.",
        usage="%(prog)s [OPTION]... TARGET...")
    parser.add_argument(
        "-a", "--aeidon",
        action="store_true",
        dest="aeidon",
        help="use python3-aeidon to process subtitles")
    parser.add_argument(
        "-f", "--file",
        action="append",
        dest="pattern_files",
        help="obtain matches from FILE")
    parser.add_argument(
        "--fix",
        action="store_true",
        dest="fix",
        help="repair potentially damaged subtitle files with aeidon")
    parser.add_argument(
        "-g", "--gui",
        action="store_true",
        dest="gui",
        help="indicate use from a GUI")
    parser.add_argument(
        "-h", "--help",
        action="help",
        help=argparse.SUPPRESS)
    parser.add_argument(
        "-r", "--regex",
        action="store_true",
        dest="regex",
        help="perform regex matching")
    parser.add_argument(
        "-s", "--case-sensitive",
        action="store_true",
        default=False,
        dest="case_sensitive",
        help="match case-sensitively")
    parser.add_argument(
        "-y", "--yes",
        action="store_true",
        dest="autoyes",
        help="automatic yes to prompts")
    parser.add_argument(
        "--version",
        action="version",
        version='%(prog)s ' + __version__)
    parser.add_argument(
        dest="targets",
        help=argparse.SUPPRESS,
        nargs="*")

    options = parser.parse_args(args)
    arguments = options.targets

    return options, arguments

def pattern_logic_aeidon():
    """Return patterns to be used for searching subtitles via aeidon."""

    if Config.options.pattern_files:
        return prep_patterns(Config.options.pattern_files)
    elif Config.options.regex:
        return Config.REGEX
    else:
        return Config.TERMS

def pattern_logic_srt():
    """Return patterns to be used for searching srt subtitles."""

    if Config.options.pattern_files and Config.options.regex:
        return prep_regex(prep_patterns(Config.options.pattern_files))
    elif Config.options.pattern_files:
        return prep_patterns(Config.options.pattern_files)
    elif Config.options.regex:
        return prep_regex(Config.REGEX)
    else:
        return Config.TERMS

def prep_files(paths, extensions):
    """Parses `paths` (which may consist of files and/or directories).
    Removes duplicates, sorts, and returns verified srt files."""

    from batchpath import GeneratePaths

    filenames = GeneratePaths().files(paths, os.W_OK, extensions, 0, True)

    if filenames:
        return filenames
    else:
        LOGGER.error('No valid targets were specified')
        sys.exit(1)

def prep_patterns(filenames):
    """Load pattern files passed via options and return list of patterns."""

    patterns = []

    for filename in filenames:
        try:
            with open(filename) as file:
                patterns += [l.rstrip('\n') for l in file]
        except:  # pylint: disable=W0702
            LOGGER.error("Unable to load pattern file '%s'" % filename)
            sys.exit(1)

    if patterns:
        # return a set to eliminate duplicates
        return set(patterns)
    else:
        LOGGER.error('No terms were loaded')
        sys.exit(1)

def prep_regex(patterns):
    """Compile regex patterns."""

    flags = 0 if Config.options.case_sensitive else re.I

    return [re.compile(pattern, flags) for pattern in patterns]

def prerequisites():
    """Display information about obtaining the aeidon module."""

    url = "http://home.gna.org/gaupol/download.html"
    debian = "sudo apt-get install python3-aeidon"
    other = "python3 setup.py --user --without-gaupol clean install"

    LOGGER.error(
        "The aeidon module is missing!\n\n"
        "Try '{0}' or the appropriate command for your package manager.\n\n"
        "You can also download the tarball for gaupol (which includes "
        "aeidon) at {1}. After downloading, unpack and run '{2}'."
        .format(debian, url, other))

def remove_elements(target, indices):
    """Remove multiple elements from a list and return result.
    This implementation is faster than the alternative below.
    Also note the creation of a new list to avoid altering the
    original. We don't have any current use for the original
    intact list, but may in the future..."""

    copied = list(target)

    for index in reversed(indices):
        del copied[index]
    return copied

# def remove_elements(target, indices):
#     """Remove multiple elements from a list and return result."""
#     return [e for i, e in enumerate(target) if i not in indices]

def start_aeidon():
    """Prepare filenames and patterns then process subtitles with aeidon."""

    extensions = ['ass', 'srt', 'ssa', 'sub']
    Config.filenames = prep_files(Config.args, extensions)

    Config.patterns = pattern_logic_aeidon()

    for filename in Config.filenames:
        AeidonProject(filename)

def start_srt():
    """Prepare filenames and patterns then process srt subtitles."""

    extensions = ['srt']
    Config.filenames = prep_files(Config.args, extensions)

    Config.patterns = pattern_logic_srt()

    for filename in Config.filenames:
        SrtProject(filename)