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

import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol";
import {ERC1155} from "@openzeppelin/token/ERC1155/ERC1155.sol";
import {IERC1155Receiver} from "@openzeppelin/interfaces/IERC1155Receiver.sol";

contract Counter is ERC1155 {
    using SafeERC20 for IERC20;

    struct Collection {
        // TODO: Add collection fee.
        uint32 numTotalSubscribers;
        uint32 totalContentFeeBips;
        mapping(uint256 => CollectionEntry) entries;
        // subscriptions is a map of address to user subscription info.
        mapping(address => SubscriptionInfo) subscriptions;
    }

    struct CollectionEntry {
        // lastNumSubscribersPayout is the last number of collection subscribers a payment
        // was triggered at.
        uint32 lastNumSubscribersPayout;
        // feePips represents how much this address gets paid from a subscription.
        // This is paid out by the following calculation:
        // `fee_bips / 10,000 * base_subscription_price * num_subscriptions`.
        // Note that `fee_bips` can be greater than 10,000 if an artist has more than one
        // piece of content added to the collection.
        uint32 feeBips;
    }

    struct SubscriptionInfo {
        uint32 subscriptionStartTimestampSeconds;
        uint32 subscriptionDurationSeconds;
    }

    // EVENTS.
    event ClaimedRewards(uint256 creatorId, address receiver, uint256 amount);
    event CollectionCreated(
        address collectionOwner,
        uint32 collectionIndex
    );
    event CollectionEntryUpdated(
        address collectionOwner,
        uint32 collectionIndex,
        uint256 creatorId,
        uint32 feeBips
    );
    event UserSubscribed(
        address subscriber,
        address collectionOwner,
        uint32 collectionIndex,
        uint32 subscriptionStartTimestamp,
        uint32 subscriptionDuration,
        uint256 subscriptionFee
    );

    // collections is a mapping of collection owner to a list of their collections.
    mapping(address => Collection[]) public collections;

    mapping(uint256 => uint256) public creatorIdToTotalUsdcEarned;
    mapping(address => mapping(uint256 => uint256))
        public addressToCreatorIdToClaimedUsdc;

    uint256 public number;
    uint256 public numCreators;

    // CONSTANTS.
    uint16 TOTAL_BIPS = 10_000;
    uint256 TOKEN_SUPPLY = 1_000_000_000 * 10**18;

    // IMMUTABLES.
    IERC20 public immutable USDC_ADDRESS;
    uint256 public immutable SUBSCRIPTION_FEE_USDC;
    uint32 public immutable SUBSCRIPTION_LENGTH_SECONDS;

    constructor(
        IERC20 usdcAddress,
        uint256 subscriptionFeeUsdc,
        uint32 subscriptionLengthSeconds,
        string memory uri
    ) ERC1155(uri) {
        USDC_ADDRESS = usdcAddress;
        SUBSCRIPTION_FEE_USDC = subscriptionFeeUsdc;
        SUBSCRIPTION_LENGTH_SECONDS = subscriptionLengthSeconds;
    }

    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }

    function increment() public {
        number++;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC1155)
        returns (bool)
    {
        return
            interfaceId == type(IERC1155Receiver).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    function createCollection() public {
        address owner = msg.sender;
        Collection[] storage collectionArray = collections[owner];
        uint256 collectionArrayLength = collectionArray.length;

        // TODO: This check is only necessary because collectionIndex is a `uint32`,
        // should consider removing.
        require(collectionArrayLength < type(uint32).max);

        collectionArray.push();

        emit CollectionCreated(owner, uint32(collectionArrayLength));
    }

    function getCollection(address collectionOwner, uint32 collectionIndex)
        internal
        view
        returns (Collection storage)
    {
        return collections[collectionOwner][collectionIndex];
    }

    function getCollectionNumTotalSubscribers(
        address collectionOwner,
        uint32 collectionIndex
    ) public view returns (uint32) {
        Collection storage collection = getCollection(
            collectionOwner,
            collectionIndex
        );
        return collection.numTotalSubscribers;
    }

    function getCollectionTotalFeeBips(
        address collectionOwner,
        uint32 collectionIndex
    ) public view returns (uint32) {
        Collection storage collection = getCollection(
            collectionOwner,
            collectionIndex
        );
        return collection.totalContentFeeBips;
    }

    function updateCollectionEntry(
        uint32 collectionIndex,
        uint256 creatorId,
        uint32 newFeeBips
    ) public {
        address owner = msg.sender;
        Collection storage collection = getCollection(owner, collectionIndex);

        CollectionEntry storage entry = getSettledCollectionEntry(
            collection,
            creatorId
        );

        // Return early if we aren't updating the fees.
        if (entry.feeBips == newFeeBips) {
            return;
        }

        // If we are increasing the fee bips, add the diff onto the collection fee bips.
        // Else we are decreasing the fee bips, subtract the diff from the collection fee bips.
        uint32 feeBips = entry.feeBips;
        if (newFeeBips > feeBips) {
            collection.totalContentFeeBips += newFeeBips - feeBips;
        } else {
            collection.totalContentFeeBips -= feeBips - newFeeBips;
        }

        // TODO: Emit event.
        entry.feeBips = newFeeBips;
        emit CollectionEntryUpdated(owner, collectionIndex, creatorId, newFeeBips);
    }

    function getCollectionEntry(
        address collectionOwner,
        uint32 collectionIndex,
        uint256 creatorId
    ) public view returns (CollectionEntry memory) {
        Collection storage collection = getCollection(
            collectionOwner,
            collectionIndex
        );
        CollectionEntry storage entry = collection.entries[creatorId];
        return entry;
    }

    function getSettledCollectionEntry(
        Collection storage collection,
        uint256 creatorId
    ) internal returns (CollectionEntry storage) {
        // TODO: Verify `creatorId` exists.
        CollectionEntry storage entry = collection.entries[creatorId];

        // Return early if the collection does not need to be settled.
        if (entry.lastNumSubscribersPayout == collection.numTotalSubscribers) {
            return entry;
        }

        // If this entry is earning fees, it needs to be settled.
        if (entry.feeBips > 0) {
            uint32 numNewSubscribers = collection.numTotalSubscribers -
                entry.lastNumSubscribersPayout;
            uint256 paymentRequired = (numNewSubscribers *
                SUBSCRIPTION_FEE_USDC *
                entry.feeBips) / TOTAL_BIPS;
            creatorIdToTotalUsdcEarned[creatorId] += paymentRequired;
        }

        // TODO: This always updates the payout even if a payment didn't happen. Verify that this isn't an issue.
        // Largest possible rounding error is $0.01 USDC.
        entry.lastNumSubscribersPayout = collection.numTotalSubscribers;
        return entry;
    }

    function subscribeToCollection(
        address collectionOwner,
        uint32 collectionIndex,
        address subscriber,
        address payer
    ) public {
        Collection storage collection = getCollection(
            collectionOwner,
            collectionIndex
        );
        SubscriptionInfo storage subscriptionInfo = collection.subscriptions[
            subscriber
        ];

        uint32 subscriptionEndTimestampSeconds = subscriptionInfo
            .subscriptionStartTimestampSeconds +
            subscriptionInfo.subscriptionDurationSeconds;
        // Require the current subscription has ended.
        // TODO: Allow extending a subscription.
        require(subscriptionEndTimestampSeconds < block.timestamp);

        uint256 subscriptionFee = (SUBSCRIPTION_FEE_USDC *
            collection.totalContentFeeBips) / TOTAL_BIPS;

        USDC_ADDRESS.safeTransferFrom(payer, address(this), subscriptionFee);
        // TODO: Allow user to specify subscription start timestamp in the future.
        subscriptionInfo.subscriptionStartTimestampSeconds = uint32(
            block.timestamp
        );
        subscriptionInfo
            .subscriptionDurationSeconds = SUBSCRIPTION_LENGTH_SECONDS;

        collection.numTotalSubscribers += 1;

        emit UserSubscribed(
            subscriber,
            collectionOwner,
            collectionIndex,
            subscriptionInfo.subscriptionStartTimestampSeconds,
            subscriptionInfo.subscriptionDurationSeconds,
            subscriptionFee
        );
    }

    function registerCreator() public {
        address creator = msg.sender;
        uint256 creatorTokenId = numCreators;
        bytes memory data;

        _mint(creator, creatorTokenId, TOKEN_SUPPLY, data);

        numCreators++;

        // TODO: Emit event.
    }

    function claimRewards(uint256 creatorId) public {
        // TODO: Throw error if `creatorId` does not exist.
        address receiver = msg.sender;
        uint256 tokenBalance = balanceOf(receiver, creatorId);
        uint256 totalHistoricalRewards = creatorIdToTotalUsdcEarned[creatorId];

        // TODO: Think about using ERC1155Supply to track total supply instead (only necessary if minting more tokens).
        // TODO: Think more about how rounding will work here. Might make sense to enforce a minimum token balance.
        uint256 totalUsdcOwed = (totalHistoricalRewards * tokenBalance) /
            TOKEN_SUPPLY;
        uint256 claimedUsdc = addressToCreatorIdToClaimedUsdc[receiver][
            creatorId
        ];

        uint256 currentUsdcOwed = totalUsdcOwed - claimedUsdc;
        require(currentUsdcOwed > 0, "claimRewards: no rewards to claim");

        // Update the claimed amount.
        addressToCreatorIdToClaimedUsdc[receiver][creatorId] = totalUsdcOwed;

        // Transfer the funds.
        USDC_ADDRESS.transfer(receiver, currentUsdcOwed);

        emit ClaimedRewards(creatorId, receiver, currentUsdcOwed);
    }
}
