// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

import {RaidGeldUtils} from "../src/RaidGeldUtils.sol";
import {Army, Player, Raider, Boss} from "../src/RaidGeldStructs.sol";
import "../src/Constants.sol";

contract RaidGeld is ERC20, Ownable, Constants {
    uint256 public constant MANTISSA = 1e4;
    uint256 public constant BUY_IN_AMOUNT = 0.00005 ether;
    uint256 public BUY_IN_DAO_TOKEN_AMOUNT;
    uint256 public constant INITIAL_GELD = 50 * MANTISSA;
    mapping(address => Player) private players;
    mapping(address => Army) private armies;
    mapping(address => Boss) private bosses;

    // WETH
    IWETH public immutable weth = IWETH(WETH);
    // RGCVII token
    ERC20 public daoToken;
    // RGCVII pool
    address public pool;
    // Uniswap
    ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER);
    // Events

    event PlayerRegistered(address indexed player, uint256 initialGeld);
    event PlayerStrikesAgain(address indexed player, uint256 totalRewards, uint256 initialGeld);
    event RaidPerformed(address indexed player, uint256 totalMinted, uint256 geldBalance);
    event UnitAdded(
        address indexed player,
        uint8 unitType,
        uint16 nUnits,
        uint256 cost,
        uint256 geldBalance,
        uint16 molochDenierLevel,
        uint16 apprenticeLevel,
        uint16 anointedLevel,
        uint16 championLevel
    );
    event DaoTokenBuyInAmountSet(address indexed owner, uint256 oldAmount, uint256 newAmount);
    event PrestigeGained(address indexed player, uint32 prestigeLevel);
    event BossDefeated(address indexed player, uint8 bossLevel, uint256 earnings);

    // Modifier for functions that should only be available to registered players
    modifier onlyPlayer() {
        require(players[msg.sender].created_at != 0, "Not an initiated player");
        _;
    }

    modifier onlyActiveSession() {
        require(players[msg.sender].has_active_session, "Session is not active, you need to buy into the game first");
        _;
    }

    modifier newPlay() {
        Player storage player = players[msg.sender];
        bool notRegistered = player.created_at == 0;
        bool returningPlayer = player.is_registered && !player.has_active_session;
        require(notRegistered || returningPlayer, "Active session already in progress");
        _;
    }

    constructor(address _daoToken, address _pool) ERC20("Raid Geld", "GELD") Ownable(msg.sender) {
        daoToken = ERC20(_daoToken);
        pool = _pool;
        BUY_IN_DAO_TOKEN_AMOUNT = 500 * 10 ** daoToken.decimals();
    }

    function start_game(address player) private {
        bool existing_player = players[player].is_registered;

        // Mint some starting tokens to the player
        _mint(player, INITIAL_GELD);
        // Reset or set player
        resetPlayer(player);
        players[player].has_active_session = true;
        players[player].is_registered = true;
        players[player].n_runs += 1;

        if (existing_player) {
            emit PlayerStrikesAgain(player, players[player].total_rewards, INITIAL_GELD);
        } else {
            // Emit event
            emit PlayerRegistered(msg.sender, INITIAL_GELD);
        }
    }

    function resetPlayer(address _playerAddress) private {
        Player storage player = players[_playerAddress];
        // Set initial states
        
        player.total_minted = INITIAL_GELD;
        player.created_at = block.timestamp;
        player.last_raided_at = block.timestamp;
        player.has_active_session = false;
        
        armies[_playerAddress] = Army({
            moloch_denier: Raider({level: 0}),
            apprentice: Raider({level: 0}),
            anointed: Raider({level: 0}),
            champion: Raider({level: 0}),
            profit_per_second: 0
        });
        bosses[_playerAddress] = Boss({level: 0, variants: RaidGeldUtils.generate_boss_variants(block.prevrandao)});
    }

    // New player want to register with ETH
    function register_eth() external payable newPlay {
        require(msg.value == BUY_IN_AMOUNT, "Incorrect buy in amount");
        weth.deposit{value: BUY_IN_AMOUNT}();
        weth.approve(address(router), BUY_IN_AMOUNT);
        ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02.ExactInputSingleParams({
            tokenIn: WETH,
            tokenOut: DAO_TOKEN,
            fee: 10000,
            recipient: address(this),
            amountIn: BUY_IN_AMOUNT,
            amountOutMinimum: 0,
            sqrtPriceLimitX96: 0
        });
        router.exactInputSingle(params);
        start_game(msg.sender);
    }

    // New player wants to register with dao
    function register_dao() external payable newPlay {
        //@notice this is not safe for arbitrary tokens, which may not follow the interface eg. USDT
        //@notice but should be fine for the DAO token
        require(
            daoToken.transferFrom(msg.sender, address(this), BUY_IN_DAO_TOKEN_AMOUNT), "Failed to transfer DAO tokens"
        );
        start_game(msg.sender);
    }

    // Override for default number of decimals
    function decimals() public view virtual override returns (uint8) {
        return 4;
    }

    // Manual minting for itchy fingers
    function raid() external onlyActiveSession {
        performRaid(msg.sender);
    }

    // Helper so we can use it when buying units too
    function performRaid(address player) private {
        uint256 time_past = block.timestamp - players[player].last_raided_at;
        uint256 new_geld = armies[player].profit_per_second * time_past;
        _mint(player, new_geld);
        players[player].last_raided_at = block.timestamp;
        players[player].total_minted += new_geld;
        emit RaidPerformed(player, players[player].total_minted, balanceOf(player));
    }

    // Function to get the Player struct
    function getPlayer(address addr) public view returns (Player memory) {
        return players[addr];
    }

    // Function to get the Army struct
    function getArmy(address addr) public view returns (Army memory) {
        return armies[addr];
    }

    // Function to get the Boss struct
    function getBoss(address addr) public view returns (Boss memory) {
        return bosses[addr];
    }

    // Quick fn to check if user is registered
    function isRegistered(address addr) public view returns (bool) {
        return players[addr].created_at != 0;
    }

    // Add a unit to your army
    function addUnit(uint8 unit, uint16 n_units) external onlyActiveSession {
        require(unit <= 3, "Unknown unit");

        Army storage army = armies[msg.sender];
        uint16 currentLevel = 0;
        if (unit == 0) {
            // moloch_denier
            currentLevel = army.moloch_denier.level;
        } else if (unit == 1) {
            // apprentice
            currentLevel = army.apprentice.level;
        } else if (unit == 2) {
            // anointed
            currentLevel = army.anointed.level;
        } else if (unit == 3) {
            // champion
            currentLevel = army.champion.level;
        }

        uint256 cost = RaidGeldUtils.calculateUnitPrice(unit, currentLevel, n_units);


        performRaid(msg.sender);

        // TODO: Since we are first minting then burning the token, this could be simplified
        require(balanceOf(msg.sender) >= cost, "Not enough GELD to add this unit");

        // then burn the cost of the new army
        _burn(msg.sender, cost);

        // Increase level
        if (unit == 0) {
            // moloch_denier
            army.moloch_denier.level += n_units;
        } else if (unit == 1) {
            // apprentice
            army.apprentice.level += n_units;
        } else if (unit == 2) {
            // anointed
            army.anointed.level += n_units;
        } else if (unit == 3) {
            // champion
            army.champion.level += n_units;
        }

        // update profite per second
        army.profit_per_second = RaidGeldUtils.calculateProfitsPerSecond(army);

        // Emit event
        emit UnitAdded(
            msg.sender,
            unit,
            n_units,
            cost,
            balanceOf(msg.sender),
            army.moloch_denier.level,
            army.apprentice.level,
            army.anointed.level,
            army.champion.level
        );
    }

    function battle_with_boss() external onlyActiveSession returns (bool[2] memory hasWonOrAscended) {
        // first perform raid
        performRaid(msg.sender);
        Boss memory boss_to_attack = bosses[msg.sender];
        // calculate how much the player will put into battle
        uint256 geld_to_burn = balanceOf(msg.sender) >= RaidGeldUtils.getBossPower(boss_to_attack.level)
            ? RaidGeldUtils.getBossPower(boss_to_attack.level)
            : balanceOf(msg.sender);
        bool hasWonBattle = RaidGeldUtils.calculateBossFight(boss_to_attack.level, geld_to_burn, block.prevrandao);
        if (hasWonBattle) {
            // Burn geld, send some sweet DAO Token and continue
            _burn(msg.sender, geld_to_burn);
            uint256 reward = RaidGeldUtils.calculateBossReward(boss_to_attack.level, BUY_IN_DAO_TOKEN_AMOUNT);
            players[msg.sender].total_rewards += reward;
            daoToken.transfer(msg.sender, reward);
            emit BossDefeated(msg.sender, boss_to_attack.level, reward);

            if (boss_to_attack.level == 6) {
                // User ascends! Moloch is defeated, user can start a new run
                players[msg.sender].prestige_level += 1;
                emit PrestigeGained(msg.sender, players[msg.sender].prestige_level);
                player_dies(msg.sender);
                return [hasWonBattle, true /* New prestige level! */ ];
            } else {
                // else go to next boss
                bosses[msg.sender].level += 1;
            }
        } else {
            // Whoops u died, boss defeated you
            player_dies(msg.sender);
        }
        return [hasWonBattle, false /* hasnt gotten prestige level */ ];
    }

    function player_dies(address player) private {
        resetPlayer(player);
        players[player].has_active_session = false;
        _burn(msg.sender, balanceOf(player));
    }

    function setDaoTokenBuyInAmount(uint256 newAmount) external onlyOwner {
        require(newAmount > 0, "Amount must be greater than 0");
        emit DaoTokenBuyInAmountSet(msg.sender, BUY_IN_DAO_TOKEN_AMOUNT, newAmount);
        BUY_IN_DAO_TOKEN_AMOUNT = newAmount;
    }

    receive() external payable {
        revert("No plain Ether accepted, use register() function to check in :)");
    }

    // Revert any non-function-call Ether transfers or calls to non-existent functions
    fallback() external payable {
        revert("No fallback calls accepted");
    }
}

interface ISwapRouter02 {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

    function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);

    struct ExactOutputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountOut;
        uint256 amountInMaximum;
        uint160 sqrtPriceLimitX96;
    }

    function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn);
}

interface IWETH is IERC20 {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}
