// 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';
import {MerkleProof} from '@openzeppelin/utils/cryptography/MerkleProof.sol';

contract Splendid is ERC1155 {
  using SafeERC20 for IERC20;

  struct MerkleTreeNode {
    uint256 creatorId;
    uint224 lastUpdatedEpoch;
    uint32 currentFeeBips;
  }

  struct ProposedMerkleTreeRoot {
    bytes32 root;
    uint32 totalContentFeeBips;
  }

  // TODO: Think about struct packing.
  struct Collection {
    // subscriptions is a map of address to user subscription info.
    mapping(address => SubscriptionInfo) subscriptions;
    // Merkle roots of each epoch in ascending order (first epoch to current epoch).
    // Payments can only be processed for historical roots (I.E., not the last root in this list).
    // The current epoch number is the length of this list minus one (zero-indexed).
    mapping(uint224 => bytes32) epochNumberToFinalizedRoot;
    mapping(uint256 => uint224) creatorIdToFirstUnclaimedEpoch;
    mapping(uint224 => uint256) epochNumberToTotalCumulativeSubscriptions;
    // Proposed root of the following epoch, which will be finalized at the beginning of the next epoch.
    ProposedMerkleTreeRoot proposedRoot;
    // Length of each epoch in seconds.
    uint32 epochLength;
    // The starting timestamp of the first epoch.
    uint32 epochZeroStartTime;
    uint32 currentTotalContentFeeBips;
    uint224 curatorFirstUnclaimedEpoch;
    // Current epoch of this collection.
    uint224 currentEpoch;
    uint256 curatorFee;
  }

  struct SubscriptionInfo {
    uint32 subscriptionStartTimestampSeconds;
    uint32 subscriptionDurationSeconds;
  }

  // EVENTS.
  event SettledCreatorEpochRewardsForCollection(
    address collectionOwner,
    uint32 collectionIndex,
    uint256 creatorId,
    uint256 startingEpoch,
    uint256 endingEpoch,
    uint256 rewardsSettled
  );
  event CreatorClaimedRewards(uint256 creatorId, address receiver, uint256 amount);
  event CuratorClaimedRewards(address collectionOwner, uint32 collectionIndex, uint256 amount);
  event CollectionCreated(
    address collectionOwner,
    uint32 collectionIndex,
    uint256 curatorFee,
    bytes32 initialRoot,
    uint32 initialContentFeeBips,
    uint32 epochLength,
    uint32 epochZeroStartTime
  );
  event CollectionRootUpdated(
    address collectionOwner,
    uint32 collectionIndex,
    bytes32 root,
    uint32 totalContentFeeBips,
    uint224 epoch,
    bool isActiveRoot
  );
  event UserSubscribed(
    address subscriber,
    address collectionOwner,
    uint32 collectionIndex,
    uint32 subscriptionStartTimestamp,
    uint32 subscriptionDuration,
    uint256 subscriptionFee
  );
  event CreatorRegistered(
    address creator,
    uint256 creatorId
  );

  // 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 numCreators;

  // CONSTANTS.
  uint16 public TOTAL_BIPS = 10_000;
  uint256 public 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;
  }

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

  function createCollection(
    uint256 curatorFee,
    bytes32 initialRoot,
    uint32 initialContentFeeBips,
    uint32 epochLength
  ) 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);

    // TODO: Enforce that fee bips is always a multiple of TOTAL_BIPS.

    collectionArray.push();
    Collection storage collection = collectionArray[collectionArrayLength];
    collection.epochNumberToFinalizedRoot[0] = initialRoot;
    collection.currentEpoch = 0;
    collection.currentTotalContentFeeBips = initialContentFeeBips;
    collection.proposedRoot = ProposedMerkleTreeRoot({root: initialRoot, totalContentFeeBips: initialContentFeeBips});
    collection.epochLength = epochLength;
    collection.epochZeroStartTime = uint32(block.timestamp);
    collection.curatorFee = curatorFee;

    emit CollectionCreated(
      owner,
      uint32(collectionArrayLength),
      curatorFee,
      initialRoot,
      initialContentFeeBips,
      epochLength,
      collection.epochZeroStartTime
    );
  }

  // TODO: Add functionality to allow curator to update the collection curator fee.
  function updateCollectionProposedRootForEpoch(
    uint32 collectionIndex,
    bytes32 newProposedRoot,
    uint32 newTotalContentFeeBips,
    // TODO: Is specifying epoch good from a product perspective?
    uint224 epochToUpdate
  ) public {
    address collectionOwner = msg.sender;
    Collection storage collection = getCollectionAndSettleRoot(collectionOwner, collectionIndex);

    // Verify we're updating the correct epoch.
    require(collection.currentEpoch == epochToUpdate);

    // Update the proposed root.
    collection.proposedRoot = ProposedMerkleTreeRoot({root: newProposedRoot, totalContentFeeBips: newTotalContentFeeBips});

    emit CollectionRootUpdated(collectionOwner, collectionIndex, newProposedRoot, newTotalContentFeeBips, epochToUpdate, false);
  }

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

  function getCollectionAndSettleRoot(address collectionOwner, uint32 collectionIndex) internal returns (Collection storage) {
    Collection storage collection = collections[collectionOwner][collectionIndex];
    uint224 currentEpoch = (uint32(block.timestamp) - collection.epochZeroStartTime) / collection.epochLength;

    // If this collection isn't settled yet, update the root.
    if (currentEpoch > collection.currentEpoch) {
      collection.currentEpoch = currentEpoch;
      ProposedMerkleTreeRoot storage proposedRoot = collection.proposedRoot;
      collection.epochNumberToFinalizedRoot[currentEpoch] = proposedRoot.root;
      collection.currentTotalContentFeeBips = proposedRoot.totalContentFeeBips;
      emit CollectionRootUpdated(collectionOwner, collectionIndex, proposedRoot.root, proposedRoot.totalContentFeeBips, currentEpoch, true);
    }

    return collection;
  }

  function getCollectionNumTotalSubscribersAtEpoch(
    address collectionOwner,
    uint32 collectionIndex,
    uint224 epoch
  ) public view returns (uint256) {
    Collection storage collection = getCollection(collectionOwner, collectionIndex);
    return collection.epochNumberToTotalCumulativeSubscriptions[epoch];
  }

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

  function subscribeToCollection(
    address collectionOwner,
    uint32 collectionIndex,
    address subscriber,
    address payer
  ) public {
    Collection storage collection = getCollectionAndSettleRoot(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.currentTotalContentFeeBips) / TOTAL_BIPS) + collection.curatorFee;

    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;

    // Update the number of subscribers for the current epoch.
    collection.epochNumberToTotalCumulativeSubscriptions[collection.currentEpoch] += 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++;

    emit CreatorRegistered(creator, creatorTokenId);
  }

  function claimCreatorRewards(uint256 creatorId) public {
    address receiver = msg.sender;
    // TODO: This needs snapshotting.
    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, 'claimCreatorRewards: no rewards to claim');

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

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

    emit CreatorClaimedRewards(creatorId, receiver, currentUsdcOwed);
  }

  function getNumUnclaimedSubscriptionsBetweenEpochs(
    Collection storage collection,
    uint224 startingEpoch,
    uint224 endingEpoch
  ) internal view returns (uint256) {
    require(startingEpoch <= endingEpoch, 'Provided epochs overlap');

    uint256 numClaimedSubscriptions = startingEpoch == 0 ? 0 : collection.epochNumberToTotalCumulativeSubscriptions[startingEpoch - 1];
    uint256 endingEpochTotalCumulativeSubscriptions = collection.epochNumberToTotalCumulativeSubscriptions[endingEpoch];

    // Require that the number of cumulative subscriptions is always increasing to protect against underflow. If this check
    // fails it indicates the subscription accounting failed.
    // TODO: Is this already checked because underflow reverts in solidity?
    require(numClaimedSubscriptions <= endingEpochTotalCumulativeSubscriptions);
    return endingEpochTotalCumulativeSubscriptions - numClaimedSubscriptions;
  }

  function claimCuratorRewards(uint32 collectionIndex) public {
    address collectionOwner = msg.sender;

    Collection storage collection = getCollectionAndSettleRoot(collectionOwner, collectionIndex);

    // Get the number of unclaimed subscriptions. Note that we don't allow the curator to claim rewards from the current epoch.
    uint256 unclaimedSubscriptions = getNumUnclaimedSubscriptionsBetweenEpochs(
      collection,
      collection.curatorFirstUnclaimedEpoch,
      collection.currentEpoch - 1
    );

    require(unclaimedSubscriptions > 0, 'Curator has no rewards to claim');

    uint256 paymentRequired = unclaimedSubscriptions * collection.curatorFee;

    // Update the first unclaimed epoch for the curator.
    collection.curatorFirstUnclaimedEpoch = collection.currentEpoch + 1;

    // Transfer the funds.
    USDC_ADDRESS.transfer(collectionOwner, paymentRequired);

    emit CuratorClaimedRewards(collectionOwner, collectionIndex, paymentRequired);
  }

  function settleCreatorEpochRewardsFromCollection(
    address collectionOwner,
    uint32 collectionIndex,
    uint224 startingEpoch,
    MerkleTreeNode calldata startingEpochTreeNode,
    bytes32[] calldata startingEpochMerkleProof,
    uint224 endingEpoch,
    MerkleTreeNode calldata endingEpochTreeNode,
    bytes32[] calldata endingEpochMerkleProof
  ) public {
    // Require the creator IDs for both nodes are the same.
    require(startingEpochTreeNode.creatorId == endingEpochTreeNode.creatorId, 'Creator IDs are not the same');
    // Require that the next node has the correct previous epoch.
    require(startingEpoch == endingEpochTreeNode.lastUpdatedEpoch, 'Starting epoch does not match the specified last updated epoch');

    Collection storage collection = getCollectionAndSettleRoot(collectionOwner, collectionIndex);

    // Require that we haven't already claimed funds for this epoch.
    require(
      collection.creatorIdToFirstUnclaimedEpoch[startingEpochTreeNode.creatorId] == startingEpoch,
      'Already settled creator rewards for this epoch'
    );
    // Disallow claiming rewards from the current epoch.
    require(endingEpoch < collection.currentEpoch, 'Cannot settle creator rewards from current epoch');

    // Verify the merkle proof of the previous root.
    verifyMerkleProof(collection.epochNumberToFinalizedRoot[startingEpoch], startingEpochTreeNode, startingEpochMerkleProof);

    // Verify the merkle proof of the next root.
    verifyMerkleProof(collection.epochNumberToFinalizedRoot[endingEpoch], endingEpochTreeNode, endingEpochMerkleProof);

    // Both merkle proofs are valid. Get the number of new subscriptions that are being claimed.
    uint256 numUnclaimedSubscriptions = getNumUnclaimedSubscriptionsBetweenEpochs(collection, startingEpoch, endingEpoch);

    // Calculate the number of subscriptions that happened between both epochs to perform the payout.
    // TODO: Think more about how rounding will work here. Might make sense to enforce a minimum token balance.
    uint256 paymentRequired = (numUnclaimedSubscriptions * SUBSCRIPTION_FEE_USDC * startingEpochTreeNode.currentFeeBips) / TOTAL_BIPS;
    creatorIdToTotalUsdcEarned[startingEpochTreeNode.creatorId] += paymentRequired;

    // Update the first unclaimed epoch for this creator.
    collection.creatorIdToFirstUnclaimedEpoch[startingEpochTreeNode.creatorId] = endingEpoch + 1;

    emit SettledCreatorEpochRewardsForCollection(
      collectionOwner,
      collectionIndex,
      startingEpochTreeNode.creatorId,
      startingEpoch,
      endingEpoch,
      paymentRequired
    );
  }

  function verifyMerkleProof(
    bytes32 merkleRoot,
    MerkleTreeNode calldata epochTreeNode,
    bytes32[] calldata epochMerkleProof
  ) public pure {
    // Verify the Merkle proof is included in the previous tree.
    bytes32 nodeHash = keccak256(abi.encodePacked(epochTreeNode.creatorId, epochTreeNode.lastUpdatedEpoch, epochTreeNode.currentFeeBips));

    require(MerkleProof.verify(epochMerkleProof, merkleRoot, nodeHash), 'Invalid merkle proof for epoch');
  }
}
