contract CryptoFamous is CryptoFamousBase {
    function CryptoFamous(address _storageContractAddress) public {
        godAddress = msg.sender;
        managerAddress = msg.sender;
        bursarAddress = msg.sender;
        verifierAddress = msg.sender;
        storageContract = CryptoFamousStorage(_storageContractAddress);
    }

    function() external payable {
        // just let msg.value be added to this.balance
        FallbackEtherReceived(msg.sender, msg.value);
    }

    event FallbackEtherReceived(address from, uint256 value);

    event EconomyParametersUpdated(uint128 _newMinCardPrice, uint128 _newInitialCardPrice, uint128 _newPurchasePremiumRate, uint128 _newHourlyValueDecayRate, uint128 _newOwnerTakeShare, uint128 _newCardTakeShare, uint128 _newPlatformFeeRate);

    /// @dev Fired whenever a Card is stolen.
    event CardStealCompleted(uint256 indexed cardId, address claimer, uint128 oldPrice, uint128 sentValue, address indexed prevOwner, address indexed newOwner, uint128 totalOwnerPayout, uint128 totalCardPayout);

    /// @dev Fired whenever a Card is claimed.
    event CardClaimCompleted(uint256 indexed cardId, address previousClaimer, address newClaimer, address indexed owner);

    /// @dev Fired whenever a Card's perk text is updated.
    event CardPerkTextUpdated(uint256 indexed cardId, string newPerkText);

    /// @notice Reference to the contract that handles the creation and ownership changes between cards.
    CryptoFamousStorage public storageContract;

    uint16 private constant TWITTER = 1;

    // solhint-disable-next-line var-name-mixedcase
    uint128 public MIN_CARD_PRICE = 0.01 ether;
    function _setMinCardPrice(uint128 _newMinCardPrice) private {
        MIN_CARD_PRICE = _newMinCardPrice;
    }

    // solhint-disable-next-line var-name-mixedcase
    uint128 public INITIAL_CARD_PRICE = 0.01 ether;
    function _setInitialCardPrice(uint128 _newInitialCardPrice) private {
        INITIAL_CARD_PRICE = _newInitialCardPrice;
    }

    // solhint-disable-next-line var-name-mixedcase
    uint128 public PURCHASE_PREMIUM_RATE = 10000; // basis points OF LAST SALE PRICE
    function _setPurchasePremiumRate(uint128 _newPurchasePremiumRate) private {
        PURCHASE_PREMIUM_RATE = _newPurchasePremiumRate;
    }

    // solhint-disable-next-line var-name-mixedcase
    uint128 public HOURLY_VALUE_DECAY_RATE = 21; // basis points OF STARTING PRICE
    function _setHourlyValueDecayRate(uint128 _newHourlyValueDecayRate) private {
        HOURLY_VALUE_DECAY_RATE = _newHourlyValueDecayRate;
    }

    // solhint-disable var-name-mixedcase
    uint128 public OWNER_TAKE_SHARE = 5000; // basis points OF PROFIT
    uint128 public CARD_TAKE_SHARE = 5000; // basis points OF PROFIT
    // solhint-enable var-name-mixedcase

    function _setProfitSharingParameters(uint128 _newOwnerTakeShare, uint128 _newCardTakeShare) private {
      require(_newOwnerTakeShare + _newCardTakeShare == 10000);

      OWNER_TAKE_SHARE = _newOwnerTakeShare;
      CARD_TAKE_SHARE = _newCardTakeShare;
    }

    // solhint-disable-next-line var-name-mixedcase
    uint128 public PLATFORM_FEE_RATE = 600; // basis points OF PROFIT
    function _setPlatformFeeRate(uint128 _newPlatformFeeRate) private {
        require(_newPlatformFeeRate < 10000);
        PLATFORM_FEE_RATE = _newPlatformFeeRate;
    }

    /// @dev Used to update all the parameters of the economy in one go
    function setEconomyParameters(uint128 _newMinCardPrice, uint128 _newInitialCardPrice, uint128 _newPurchasePremiumRate, uint128 _newHourlyValueDecayRate, uint128 _newOwnerTakeShare, uint128 _newCardTakeShare, uint128 _newPlatformFeeRate) external requireAdmin {
        _setMinCardPrice(_newMinCardPrice);
        _setInitialCardPrice(_newInitialCardPrice);
        _setPurchasePremiumRate(_newPurchasePremiumRate);
        _setHourlyValueDecayRate(_newHourlyValueDecayRate);
        _setProfitSharingParameters(_newOwnerTakeShare, _newCardTakeShare);
        _setPlatformFeeRate(_newPlatformFeeRate);
        EconomyParametersUpdated(_newMinCardPrice, _newInitialCardPrice, _newPurchasePremiumRate, _newHourlyValueDecayRate, _newOwnerTakeShare, _newCardTakeShare, _newPlatformFeeRate);
    }

    address public verifierAddress;
    /// @notice Assigns a new address to act as the Verifier. Only available to the current God.
    /// @notice The Verifier address is used to confirm the authenticity of the claim signature.
    /// @param _newVerifier The address of the new Verifier
    function setVerifier(address _newVerifier) external requireGod {
        require(_newVerifier != address(0));

        verifierAddress = _newVerifier;
    }

    // mimicking eth_sign.
    function prefixed(bytes32 hash) private pure returns (bytes32) {
        return keccak256("\x19Ethereum Signed Message:\n32", hash);
    }

    function claimTwitterId(uint256 _twitterId, address _claimerAddress, uint8 _v, bytes32 _r, bytes32 _s) external whenNotPaused returns (uint256) {
      return _claimSocialNetworkIdentity(TWITTER, _twitterId, _claimerAddress, _v, _r, _s);
    }

    function claimSocialNetworkIdentity(uint256 _socialNetworkType, uint256 _socialId, address _claimerAddress, uint8 _v, bytes32 _r, bytes32 _s) external whenNotPaused returns (uint256) {
      return _claimSocialNetworkIdentity(_socialNetworkType, _socialId, _claimerAddress, _v, _r, _s);
    }

    /// @dev claiming a social identity requires a signature provided by the CryptoFamous backend
    /// to verify the authenticity of the claim. Once a Card is claimed by an address, that address
    /// has access to the Card's current and future earnings on the system.
    function _claimSocialNetworkIdentity(uint256 _socialNetworkType, uint256 _socialId, address _claimerAddress, uint8 _v, bytes32 _r, bytes32 _s) private returns (uint256) {
      uint8 _socialNetworkType8 = uint8(_socialNetworkType);
      require(_socialNetworkType == uint256(_socialNetworkType8));

      uint64 _socialId64 = uint64(_socialId);
      require(_socialId == uint256(_socialId64));

      uint256 cardId = storageContract.socialIdentityToCardId(_socialNetworkType8, _socialId64);

      uint16 claimNonce = 0;
      if (cardId != 0) {
        (, , , claimNonce, ) = storageContract.allCards(cardId);
      }

      bytes32 prefixedAndHashedAgain = prefixed(
        keccak256(
          _socialNetworkType, _socialId, _claimerAddress, uint256(claimNonce)
        )
      );

      address recoveredSignerAddress = ecrecover(prefixedAndHashedAgain, _v, _r, _s);
      require(recoveredSignerAddress == verifierAddress);

      if (cardId == 0) {
        return storageContract.authorized_createCard(_socialNetworkType8, _socialId64, _claimerAddress, _claimerAddress);
      } else {
        _claimExistingCard(cardId, _claimerAddress);
        return cardId;
      }
    }

    function _claimExistingCard(uint256 _cardId, address _claimerAddress) private {
        address previousClaimer;
        (, , previousClaimer, ,) = storageContract.allCards(_cardId);
        address owner = storageContract.cardIdToOwner(_cardId);

        _updateCardClaimerAddress(_cardId, _claimerAddress);

        CardClaimCompleted(_cardId, previousClaimer, _claimerAddress, owner);

        uint256 stashedPayout = storageContract.cardIdToStashedPayout(_cardId);
        if (stashedPayout > 0) {
          _triggerStashedPayoutTransfer(_cardId);
        }
    }

    /// @dev The Card's perk text is displayed prominently on its profile and will likely be
    /// used for promotional reasons.
    function setCardPerkText(uint256 _cardId, string _perkText) external whenNotPaused {
      address cardClaimer;
      (, , cardClaimer, , ) = storageContract.allCards(_cardId);

      require(cardClaimer == msg.sender);

      require(bytes(_perkText).length <= 280);

      _updateCardPerkText(_cardId, _perkText);
      CardPerkTextUpdated(_cardId, _perkText);
    }

    function stealCardWithTwitterId(uint256 _twitterId) external payable whenNotPaused {
      _stealCardWithSocialIdentity(TWITTER, _twitterId);
    }

    function stealCardWithSocialIdentity(uint256 _socialNetworkType, uint256 _socialId) external payable whenNotPaused {
      _stealCardWithSocialIdentity(_socialNetworkType, _socialId);
    }

    function _stealCardWithSocialIdentity(uint256 _socialNetworkType, uint256 _socialId) private {
      // Avoid zeroth
      require(_socialId != 0);

      uint8 _socialNetworkType8 = uint8(_socialNetworkType);
      require(_socialNetworkType == uint256(_socialNetworkType8));

      uint64 _socialId64 = uint64(_socialId);
      require(_socialId == uint256(_socialId64));

      uint256 cardId = storageContract.socialIdentityToCardId(_socialNetworkType8, _socialId64);
      if (cardId == 0) {
        cardId = storageContract.authorized_createCard(_socialNetworkType8, _socialId64, address(0), address(0));
        _stealCardWithId(cardId);
      } else {
        _stealCardWithId(cardId);
      }
    }

    function stealCardWithId(uint256 _cardId) external payable whenNotPaused {
      // Avoid zeroth
      require(_cardId != 0);

      _stealCardWithId(_cardId);
    }

    function claimTwitterIdIfNeededThenStealCardWithTwitterId(
      uint256 _twitterIdToClaim,
      address _claimerAddress,
      uint8 _v,
      bytes32 _r,
      bytes32 _s,
      uint256 _twitterIdToSteal
      ) external payable whenNotPaused returns (uint256) {
          return _claimIfNeededThenSteal(TWITTER, _twitterIdToClaim, _claimerAddress, _v, _r, _s, TWITTER, _twitterIdToSteal);
      }

    function claimIfNeededThenSteal(
      uint256 _socialNetworkTypeToClaim,
      uint256 _socialIdToClaim,
      address _claimerAddress,
      uint8 _v,
      bytes32 _r,
      bytes32 _s,
      uint256 _socialNetworkTypeToSteal,
      uint256 _socialIdToSteal
      ) external payable whenNotPaused returns (uint256) {
          return _claimIfNeededThenSteal(_socialNetworkTypeToClaim, _socialIdToClaim, _claimerAddress, _v, _r, _s, _socialNetworkTypeToSteal, _socialIdToSteal);
    }

    /// @dev "Convenience" function allowing us to avoid forcing the user to go through an extra
    /// Ethereum transactions if they really, really want to do their first steal right now.
    function _claimIfNeededThenSteal(
      uint256 _socialNetworkTypeToClaim,
      uint256 _socialIdToClaim,
      address _claimerAddress,
      uint8 _v,
      bytes32 _r,
      bytes32 _s,
      uint256 _socialNetworkTypeToSteal,
      uint256 _socialIdToSteal
      ) private returns (uint256) {
        uint8 _socialNetworkTypeToClaim8 = uint8(_socialNetworkTypeToClaim);
        require(_socialNetworkTypeToClaim == uint256(_socialNetworkTypeToClaim8));

        uint64 _socialIdToClaim64 = uint64(_socialIdToClaim);
        require(_socialIdToClaim == uint256(_socialIdToClaim64));

        uint256 claimedCardId = storageContract.socialIdentityToCardId(_socialNetworkTypeToClaim8, _socialIdToClaim64);

        address currentClaimer = address(0);
        if (claimedCardId != 0) {
          (, , currentClaimer, , ) = storageContract.allCards(claimedCardId);
        }

        if (currentClaimer == address(0)) {
          claimedCardId = _claimSocialNetworkIdentity(_socialNetworkTypeToClaim, _socialIdToClaim, _claimerAddress, _v, _r, _s);
        }

        _stealCardWithSocialIdentity(_socialNetworkTypeToSteal, _socialIdToSteal);

        return claimedCardId;
    }

    function _stealCardWithId(uint256 _cardId) private { // solhint-disable-line function-max-lines, code-complexity
        // Make sure the card already exists
        uint64 twitterId;
        address cardClaimer;
        (, twitterId, cardClaimer, , ) = storageContract.allCards(_cardId);
        require(twitterId != 0);

        address oldOwner = storageContract.cardIdToOwner(_cardId);
        address newOwner = msg.sender;

        // Making sure not stealing from self
        require(oldOwner != newOwner);

        require(newOwner != address(0));

        // Check for sent value overflow (which realistically wouldn't happen)
        uint128 sentValue = uint128(msg.value);
        require(uint256(sentValue) == msg.value);

        uint128 lastPrice;
        uint128 decayedPrice;
        uint128 profit;
        // uint128 ownerProfitTake;
        // uint128 cardProfitTake;
        uint128 totalOwnerPayout;
        uint128 totalCardPayout;
        uint128 platformFee;

        (lastPrice,
        decayedPrice,
        profit,
        , // ownerProfitTake,
        , // cardProfitTake,
        totalOwnerPayout,
        totalCardPayout,
        platformFee
        ) = currentPriceInfoOf(_cardId, sentValue);

        require(sentValue >= decayedPrice);

        _updateSaleInfo(_cardId, sentValue);
        storageContract.authorized_changeOwnership(oldOwner, newOwner, _cardId);

        CardStealCompleted(_cardId, cardClaimer, lastPrice, sentValue, oldOwner, newOwner, totalOwnerPayout, totalCardPayout);

        if (platformFee > 0) {
          _recordPlatformFee(platformFee);
        }

        if (totalCardPayout > 0) {
            if (cardClaimer == address(0)) {
                _recordStashedPayout(_cardId, totalCardPayout);
            } else {
                // Because the caller can manipulate the .send to fail, we need a fallback
                if (!cardClaimer.send(totalCardPayout)) {
                  _recordStashedPayout(_cardId, totalCardPayout);
                }
            }
        }

        if (totalOwnerPayout > 0) {
          if (oldOwner != address(0)) {
              // Because the caller can manipulate the .send to fail, we need a fallback
              if (!oldOwner.send(totalOwnerPayout)) { // solhint-disable-line multiple-sends
                _recordFailedOldOwnerTransfer(oldOwner, totalOwnerPayout);
              }
          }
        }
    }

    function currentPriceInfoOf(uint256 _cardId, uint256 _sentGrossPrice) public view returns (
        uint128 lastPrice,
        uint128 decayedPrice,
        uint128 profit,
        uint128 ownerProfitTake,
        uint128 cardProfitTake,
        uint128 totalOwnerPayout,
        uint128 totalCardPayout,
        uint128 platformFee
    ) {
        uint128 lastTimestamp;
        (lastTimestamp, lastPrice) = storageContract.cardIdToSaleInfo(_cardId);

        decayedPrice = decayedPriceFrom(lastPrice, lastTimestamp);
        require(_sentGrossPrice >= decayedPrice);

        platformFee = uint128(_sentGrossPrice) * PLATFORM_FEE_RATE / 10000;
        uint128 sentNetPrice = uint128(_sentGrossPrice) - platformFee;

        if (sentNetPrice > lastPrice) {
            profit = sentNetPrice - lastPrice;
            ownerProfitTake = profit * OWNER_TAKE_SHARE / 10000;
            cardProfitTake = profit * CARD_TAKE_SHARE / 10000;
        } else {
            profit = 0;
            ownerProfitTake = 0;
            cardProfitTake = 0;
        }

        totalOwnerPayout = ownerProfitTake + (sentNetPrice - profit);
        totalCardPayout = cardProfitTake;

        // Special adjustment if there is no current owner
        address currentOwner = storageContract.cardIdToOwner(_cardId);
        if (currentOwner == address(0)) {
          totalCardPayout = totalCardPayout + totalOwnerPayout;
          totalOwnerPayout = 0;
        }

        require(_sentGrossPrice >= (totalCardPayout + totalOwnerPayout + platformFee));

        return (lastPrice, decayedPrice, profit, ownerProfitTake, cardProfitTake, totalOwnerPayout, totalCardPayout, platformFee);
    }

    function decayedPriceFrom(uint256 _lastPrice, uint256 _lastTimestamp) public view returns (uint128 decayedPrice) {
        if (_lastTimestamp == 0) {
            decayedPrice = INITIAL_CARD_PRICE;
        } else {
            uint128 startPrice = uint128(_lastPrice) + (uint128(_lastPrice) * PURCHASE_PREMIUM_RATE / 10000);
            require(startPrice >= uint128(_lastPrice));

            uint128 secondsLapsed;
            if (now > _lastTimestamp) { // solhint-disable-line not-rely-on-time
                secondsLapsed = uint128(now) - uint128(_lastTimestamp); // solhint-disable-line not-rely-on-time
            } else {
                secondsLapsed = 0;
            }
            uint128 hoursLapsed = secondsLapsed / 1 hours;
            uint128 totalDecay = (hoursLapsed * (startPrice * HOURLY_VALUE_DECAY_RATE / 10000));

            if (totalDecay > startPrice) {
                decayedPrice = MIN_CARD_PRICE;
            } else {
                decayedPrice = startPrice - totalDecay;
                if (decayedPrice < MIN_CARD_PRICE) {
                  decayedPrice = MIN_CARD_PRICE;
                }
            }
        }

        return decayedPrice;
    }

    //////////////// STORAGE CONTRACT MUTATION

    function _updateSaleInfo(uint256 _cardId, uint256 _sentValue) private {
        storageContract.authorized_updateSaleInfo(_cardId, _sentValue);
    }

    function _updateCardClaimerAddress(uint256 _cardId, address _claimerAddress) private {
        storageContract.authorized_updateCardClaimerAddress(_cardId, _claimerAddress);
    }

    function _recordStashedPayout(uint256 _cardId, uint256 _stashedPayout) private {
        storageContract.authorized_recordStashedPayout.value(_stashedPayout)(_cardId);
    }

    function _triggerStashedPayoutTransfer(uint256 _cardId) private {
        storageContract.authorized_triggerStashedPayoutTransfer(_cardId);
    }

    function _recordFailedOldOwnerTransfer(address _oldOwner, uint256 _oldOwnerPayout) private {
        storageContract.authorized_recordFailedOldOwnerTransfer.value(_oldOwnerPayout)(_oldOwner);
    }

    function _recordPlatformFee(uint256 _platformFee) private {
        storageContract.authorized_recordPlatformFee.value(_platformFee)();
    }

    function _updateCardPerkText(uint256 _cardId, string _perkText) private {
        storageContract.authorized_setCardPerkText(_cardId, _perkText);
    }

    /////////////// QUERY FUNCTIONS

    // solhint-disable-next-line func-order
    function decayedPriceOfTwitterId(uint256 _twitterId) public view returns (uint128) {
      return decayedPriceOfSocialIdentity(TWITTER, _twitterId);
    }

    function decayedPriceOfSocialIdentity(uint256 _socialNetworkType, uint256 _socialId) public view returns (uint128) {
      uint8 _socialNetworkType8 = uint8(_socialNetworkType);
      require(_socialNetworkType == uint256(_socialNetworkType8));

      uint64 _socialId64 = uint64(_socialId);
      require(_socialId == uint256(_socialId64));

      uint256 cardId = storageContract.socialIdentityToCardId(_socialNetworkType8, _socialId64);

      return decayedPriceOfCard(cardId);
    }

    function decayedPriceOfCard(uint256 _cardId) public view returns (uint128) {
      uint128 lastTimestamp;
      uint128 lastPrice;
      (lastTimestamp, lastPrice) = storageContract.cardIdToSaleInfo(_cardId);
      return decayedPriceFrom(lastPrice, lastTimestamp);
    }

    function ownerOfTwitterId(uint256 _twitterId) public view returns (address) {
      return ownerOfSocialIdentity(TWITTER, _twitterId);
    }

    function ownerOfSocialIdentity(uint256 _socialNetworkType, uint256 _socialId) public view returns (address) {
      uint8 _socialNetworkType8 = uint8(_socialNetworkType);
      require(_socialNetworkType == uint256(_socialNetworkType8));

      uint64 _socialId64 = uint64(_socialId);
      require(_socialId == uint256(_socialId64));

      uint256 cardId = storageContract.socialIdentityToCardId(_socialNetworkType8, _socialId64);

      address ownerAddress = storageContract.cardIdToOwner(cardId);
      return ownerAddress;
    }

    function claimerOfTwitterId(uint256 _twitterId) public view returns (address) {
      return claimerOfSocialIdentity(TWITTER, _twitterId);
    }

    function claimerOfSocialIdentity(uint256 _socialNetworkType, uint256 _socialId) public view returns (address) {
      uint8 _socialNetworkType8 = uint8(_socialNetworkType);
      require(_socialNetworkType == uint256(_socialNetworkType8));

      uint64 _socialId64 = uint64(_socialId);
      require(_socialId == uint256(_socialId64));

      uint256 cardId = storageContract.socialIdentityToCardId(_socialNetworkType8, _socialId64);

      address claimerAddress;
      (, , claimerAddress, ,) = storageContract.allCards(cardId);

      return claimerAddress;
    }

    function twitterIdOfClaimerAddress(address _claimerAddress) public view returns (uint64) {
      return socialIdentityOfClaimerAddress(TWITTER, _claimerAddress);
    }

    function socialIdentityOfClaimerAddress(uint256 _socialNetworkType, address _claimerAddress) public view returns (uint64) {
      uint256 cardId = storageContract.lookUpClaimerAddress(_socialNetworkType, _claimerAddress);

      uint64 socialId;
      (, socialId, , ,) = storageContract.allCards(cardId);
      return socialId;
    }

    function withdrawContractBalance(address _to) external requireBursar {
      if (_to == address(0)) {
          bursarAddress.transfer(this.balance);
      } else {
          _to.transfer(this.balance);
      }
    }
}
function decayedPriceOfTwitterId(uint256 _twitterId) public view returns (uint128) {
  return decayedPriceOfSocialIdentity(TWITTER, _twitterId);
}

// solhint-disable-next-line func-order