contract CryptoBoss is ContractAccessControl {

  address constant tokenContractAddress = 0xe1015a79a7d488f8fecf073b187d38c6f1a77368;
  ERC721SlimToken constant tokenContract = ERC721SlimToken(tokenContractAddress);

  event Participating(address indexed player, uint encounterId);
  event LootClaimed(address indexed player, uint encounterId);
  event DailyLootClaimed(uint day);

  struct ParticipantData {
    uint32 damage;
    uint64 cumulativeDamage;
    uint8 forgeWeaponRarity;
    uint8 forgeWeaponDamagePure;
    bool lootClaimed;
    bool consolationPrizeClaimed;
  }

  struct Encounter {
    mapping (address => ParticipantData) participantData;
    address[] participants;
  }

  //encounterId is the starting block number / encounterBlockDuration
  mapping (uint => Encounter) encountersById;

  mapping (uint => address) winnerPerDay;
  mapping (uint => mapping (address => uint)) dayToAddressToScore;
  mapping (uint => bool) dailyLootClaimedPerDay;

   uint constant encounterBlockDuration = 80;
   uint constant blocksInADay = 5760;

//   uint constant encounterBlockDuration = 20;
//   uint constant blocksInADay = 60;    // must be a multiple of encounterBlockDuration

  uint256 gasRefundForClaimLoot = 279032000000000;
  uint256 gasRefundForClaimConsolationPrizeLoot = 279032000000000;
  uint256 gasRefundForClaimLootWithConsolationPrize = 279032000000000;

  uint participateFee = 0.002 ether;
  uint participateDailyLootContribution = 0.001 ether;

  constructor() public {

    paused = false;

    ceoAddress = msg.sender;
    cooAddress = msg.sender;
    cfoAddress = msg.sender;
    withdrawalAddress = msg.sender;
  }
  
  function setGasRefundForClaimLoot(uint256 _gasRefundForClaimLoot) external onlyCEO {
      gasRefundForClaimLoot = _gasRefundForClaimLoot;
  }

  function setGasRefundForClaimConsolationPrizeLoot(uint256 _gasRefundForClaimConsolationPrizeLoot) external onlyCEO {
      gasRefundForClaimConsolationPrizeLoot = _gasRefundForClaimConsolationPrizeLoot;
  }

  function setGasRefundForClaimLootWithConsolationPrize(uint256 _gasRefundForClaimLootWithConsolationPrize) external onlyCEO {
      gasRefundForClaimLootWithConsolationPrize = _gasRefundForClaimLootWithConsolationPrize;
  }

  function setParticipateFee(uint _participateFee) public onlyCLevel {
    participateFee = _participateFee;
  }

  function setParticipateDailyLootContribution(uint _participateDailyLootContribution) public onlyCLevel {
    participateDailyLootContribution = _participateDailyLootContribution;
  }

  function getFirstEncounterIdFromDay(uint day) internal pure returns (uint) {
    return (day * blocksInADay) / encounterBlockDuration;
  }

  function leaderboardEntries(uint day) public view returns
    (uint etherPot, bool dailyLootClaimed, uint blockDeadline, address[] memory entryAddresses, uint[] memory entryDamages) {    

    dailyLootClaimed = dailyLootClaimedPerDay[day];
    blockDeadline = (((day+1) * blocksInADay) / encounterBlockDuration) * encounterBlockDuration;

    uint participantCount = 0;
    etherPot = 0;

    for (uint encounterId = getFirstEncounterIdFromDay(day); encounterId < getFirstEncounterIdFromDay(day+1); encounterId++)
    {
      address[] storage participants = encountersById[encounterId].participants;
      participantCount += participants.length;
      etherPot += participateDailyLootContribution * participants.length;
    }

    entryAddresses = new address[](participantCount);
    entryDamages = new uint[](participantCount);

    participantCount = 0;

    for (encounterId = getFirstEncounterIdFromDay(day); encounterId < getFirstEncounterIdFromDay(day+1); encounterId++)
    {
      participants = encountersById[encounterId].participants;
      mapping (address => ParticipantData) participantData = encountersById[encounterId].participantData;
      for (uint i = 0; i < participants.length; i++)
      {
        address participant = participants[i];
        entryAddresses[participantCount] = participant;
        entryDamages[participantCount] = participantData[participant].damage;
        participantCount++;
      }
    }
  }

  function claimDailyLoot(uint day) public {
    require(!dailyLootClaimedPerDay[day]);
    require(winnerPerDay[day] == msg.sender);

    uint firstEncounterId = day * blocksInADay / encounterBlockDuration;
    uint firstEncounterIdTomorrow = ((day+1) * blocksInADay / encounterBlockDuration);
    uint etherPot = 0;
    for (uint encounterId = firstEncounterId; encounterId < firstEncounterIdTomorrow; encounterId++)
    {
      etherPot += participateDailyLootContribution * encountersById[encounterId].participants.length;
    }

    dailyLootClaimedPerDay[day] = true;

    msg.sender.transfer(etherPot);

    emit DailyLootClaimed(day);
  }

  function blockBeforeEncounter(uint encounterId) private pure returns (uint) {
    return encounterId*encounterBlockDuration - 1;
  }

  function getEncounterDetails() public view
    returns (uint encounterId, uint encounterFinishedBlockNumber, bool isParticipating, uint day, uint monsterDna) {
    encounterId = block.number / encounterBlockDuration;
    encounterFinishedBlockNumber = (encounterId+1) * encounterBlockDuration;
    Encounter storage encounter = encountersById[encounterId];
    isParticipating = (encounter.participantData[msg.sender].damage != 0);
    day = (encounterId * encounterBlockDuration) / blocksInADay;
    monsterDna = uint(blockhash(blockBeforeEncounter(encounterId)));
  }

  function getParticipants(uint encounterId) public view returns (address[]) {

    Encounter storage encounter = encountersById[encounterId];
    return encounter.participants;
  }

  function calculateWinner(uint numParticipants, Encounter storage encounter, uint blockToHash) internal view returns
    (address winnerAddress, uint rand, uint totalDamageDealt) {

    if (numParticipants == 0) {
      return;
    }

    totalDamageDealt = encounter.participantData[encounter.participants[numParticipants-1]].cumulativeDamage;

    rand = uint(keccak256(blockhash(blockToHash)));
    uint winnerDamageValue = rand % totalDamageDealt;

    uint winnerIndex = numParticipants;

    // binary search for a value winnerIndex where
    // winnerDamageValue < cumulativeDamage[winnerIndex] and 
    // winnerDamageValue >= cumulativeDamage[winnerIndex-1]

    uint min = 0;
    uint max = numParticipants - 1;
    while(max >= min) {
      uint guess = (min+max)/2;
      if (guess > 0 && winnerDamageValue < encounter.participantData[encounter.participants[guess-1]].cumulativeDamage) {
        max = guess-1;
      }
      else if (winnerDamageValue >= encounter.participantData[encounter.participants[guess]].cumulativeDamage) {
        min = guess+1;
      } else {
        winnerIndex = guess;
        break;
      }

    }

    require(winnerIndex < numParticipants, "error in binary search");

    winnerAddress = encounter.participants[winnerIndex];
  }

  function getBlockToHashForResults(uint encounterId) public view returns (uint) {
      
    uint blockToHash = (encounterId+1)*encounterBlockDuration - 1;
    
    require(block.number > blockToHash);
    
    uint diff = block.number - (blockToHash+1);
    if (diff > 255) {
        blockToHash += (diff/256)*256;
    }
    
    return blockToHash;
  }
  
  function getEncounterResults(uint encounterId, address player) public view returns (
    address winnerAddress, uint lootTokenId, uint consolationPrizeTokenId,
    bool lootClaimed, uint damageDealt, uint totalDamageDealt) {

    uint blockToHash = getBlockToHashForResults(encounterId);

    Encounter storage encounter = encountersById[encounterId];
    uint numParticipants = encounter.participants.length;
    if (numParticipants == 0) {
      return (address(0), 0, 0, false, 0, 0);
    }

    damageDealt = encounter.participantData[player].damage;

    uint rand;
    (winnerAddress, rand, totalDamageDealt) = calculateWinner(numParticipants, encounter, blockToHash);

    lootTokenId = constructWeaponTokenIdForWinner(rand, numParticipants);

    lootClaimed = true;
    consolationPrizeTokenId = getConsolationPrizeTokenId(encounterId, player);

    if (consolationPrizeTokenId != 0) {
        lootClaimed = encounter.participantData[player].consolationPrizeClaimed;
        
        // This way has problems:
    //   lootClaimed = tokenContract.exists(consolationPrizeTokenId);
    }
  }
  
    function getLootClaimed(uint encounterId, address player) external view returns (bool, bool) {
        ParticipantData memory participantData = encountersById[encounterId].participantData[player];
        return (
            participantData.lootClaimed,
            participantData.consolationPrizeClaimed
        );
    }

  function constructWeaponTokenIdForWinner(uint rand, uint numParticipants) pure internal returns (uint) {

    uint rarity = 0;
    if (numParticipants > 1) rarity = 1;
    if (numParticipants > 10) rarity = 2;

    return constructWeaponTokenId(rand, rarity, 0);
  }

  function getWeaponRarityFromTokenId(uint tokenId) pure internal returns (uint) {
    return tokenId & 0xff;
  }  

  // damageType: 0=physical 1=magic 2=water 3=earth 4=fire
  function getWeaponDamageFromTokenId(uint tokenId, uint damageType) pure internal returns (uint) {
    return ((tokenId >> (64 + damageType*8)) & 0xff);
  }  

  function getPureWeaponDamageFromTokenId(uint tokenId) pure internal returns (uint) {
    return ((tokenId >> (56)) & 0xff);
  }  

  function getMonsterDefenseFromDna(uint monsterDna, uint damageType) pure internal returns (uint) {
    return ((monsterDna >> (64 + damageType*8)) & 0xff);
  }


  // constant lookup table

  bytes10 constant elementsAvailableForCommon =     hex"01020408100102040810";   // Each byte has 1 bit set
  bytes10 constant elementsAvailableForRare =       hex"030506090A0C11121418";   // Each byte has 2 bits set
  bytes10 constant elementsAvailableForEpic =       hex"070B0D0E131516191A1C";   // 3 bits
  bytes10 constant elementsAvailableForLegendary =  hex"0F171B1D1E0F171B1D1E";   // 4 bits

  // rarity 0: common (1 element)
  // rarity 1: rare (2 elements)
  // rarity 2: epic (3 elements)
  // rarity 3: legendary (4 elements)
  // rarity 4: ultimate (all 5 elements)
  function constructWeaponTokenId(uint rand, uint rarity, uint pureDamage) pure internal returns (uint) {
    uint lootTokenId = (rand & 0xffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000) + rarity;

    bytes10[4] memory elementsAvailablePerRarity = [
      elementsAvailableForCommon,
      elementsAvailableForRare,
      elementsAvailableForEpic,
      elementsAvailableForLegendary
      ];

    bytes10 elementsAvailable = elementsAvailablePerRarity[rarity];
    // Select a random byte in elementsAvailable
    uint8 elementsUsed = uint8(elementsAvailable[((rand >> 104) & 0xffff) % 10]);
    // The bits of elementsUsed represent which elements we will allow this weapon to deal damage for
    // Zero out the other element damages
    for (uint i = 0; i < 5; i++) {
      if ((elementsUsed & (1 << i)) == 0) {
        lootTokenId = lootTokenId & ~(0xff << (64 + i*8));
      }
    }

    pureDamage = Math.min256(100, pureDamage);

    lootTokenId = lootTokenId | (pureDamage << 56);

    return lootTokenId;
  }

  function weaponTokenIdToDamageForEncounter(uint weaponTokenId, uint encounterId) view internal returns (uint) {
    uint monsterDna = uint(blockhash(encounterId*encounterBlockDuration - 1));

    uint physicalDamage = uint(Math.smax256(0, int(getWeaponDamageFromTokenId(weaponTokenId, 0)) - int(getMonsterDefenseFromDna(monsterDna, 0))));
    uint fireDamage = uint(Math.smax256(0, int(getWeaponDamageFromTokenId(weaponTokenId, 4)) - int(getMonsterDefenseFromDna(monsterDna, 4))));
    uint earthDamage = uint(Math.smax256(0, int(getWeaponDamageFromTokenId(weaponTokenId, 3)) - int(getMonsterDefenseFromDna(monsterDna, 3))));
    uint waterDamage = uint(Math.smax256(0, int(getWeaponDamageFromTokenId(weaponTokenId, 2)) - int(getMonsterDefenseFromDna(monsterDna, 2))));
    uint magicDamage = uint(Math.smax256(0, int(getWeaponDamageFromTokenId(weaponTokenId, 1)) - int(getMonsterDefenseFromDna(monsterDna, 1))));
    uint pureDamage = getPureWeaponDamageFromTokenId(weaponTokenId);

    uint damage = physicalDamage + fireDamage + earthDamage + waterDamage + magicDamage + pureDamage;
    damage = Math.max256(1, damage);

    return damage;
  }

  function forgeWeaponPureDamage(uint sacrificeTokenId1, uint sacrificeTokenId2, uint sacrificeTokenId3, uint sacrificeTokenId4)
    internal pure returns (uint8) {
    if (sacrificeTokenId1 == 0) {
      return 0;
    }
    return uint8(Math.min256(255,
        getPureWeaponDamageFromTokenId(sacrificeTokenId1) +
        getPureWeaponDamageFromTokenId(sacrificeTokenId2) +
        getPureWeaponDamageFromTokenId(sacrificeTokenId3) +
        getPureWeaponDamageFromTokenId(sacrificeTokenId4)));
  }

  function forgeWeaponRarity(uint sacrificeTokenId1, uint sacrificeTokenId2, uint sacrificeTokenId3, uint sacrificeTokenId4)
    internal pure returns (uint8) {
    if (sacrificeTokenId1 == 0) {
      return 0;
    }
    uint rarity = getWeaponRarityFromTokenId(sacrificeTokenId1);
    rarity = Math.min256(rarity, getWeaponRarityFromTokenId(sacrificeTokenId2));
    rarity = Math.min256(rarity, getWeaponRarityFromTokenId(sacrificeTokenId3));
    rarity = Math.min256(rarity, getWeaponRarityFromTokenId(sacrificeTokenId4)) + 1;
    require(rarity < 5, "cant forge an ultimate weapon");
    return uint8(rarity);
  }

  function participate(uint encounterId, uint weaponTokenId,
    uint sacrificeTokenId1, uint sacrificeTokenId2, uint sacrificeTokenId3, uint sacrificeTokenId4) public whenNotPaused payable {
    require(msg.value >= participateFee);  // half goes to dev, half goes to ether pot

    require(encounterId == block.number / encounterBlockDuration, "a new encounter is available");

    Encounter storage encounter = encountersById[encounterId];

    require(encounter.participantData[msg.sender].damage == 0, "you are already participating");

    uint damage = 1;
    // weaponTokenId of zero means they are using their fists
    if (weaponTokenId != 0) {
      require(tokenContract.ownerOf(weaponTokenId) == msg.sender, "you dont own that weapon");
      damage = weaponTokenIdToDamageForEncounter(weaponTokenId, encounterId);
    }

    uint day = (encounterId * encounterBlockDuration) / blocksInADay;
    uint newScore = dayToAddressToScore[day][msg.sender] + damage;
    dayToAddressToScore[day][msg.sender] = newScore;

    if (newScore > dayToAddressToScore[day][winnerPerDay[day]] &&
      winnerPerDay[day] != msg.sender) {
      winnerPerDay[day] = msg.sender;
    }

    uint cumulativeDamage = damage;
    if (encounter.participants.length > 0) {
      cumulativeDamage = cumulativeDamage + encounter.participantData[encounter.participants[encounter.participants.length-1]].cumulativeDamage;
    }

    if (sacrificeTokenId1 != 0) {

      // the requires in the transfer functions here will verify
      // that msg.sender owns all of these tokens and they are unique

      // burn all four input tokens

      tokenContract.transferFrom(msg.sender, 1, sacrificeTokenId1);
      tokenContract.transferFrom(msg.sender, 1, sacrificeTokenId2);
      tokenContract.transferFrom(msg.sender, 1, sacrificeTokenId3);
      tokenContract.transferFrom(msg.sender, 1, sacrificeTokenId4);
    }

    encounter.participantData[msg.sender] = ParticipantData(uint32(damage), uint64(cumulativeDamage), 
      forgeWeaponRarity(sacrificeTokenId1, sacrificeTokenId2, sacrificeTokenId3, sacrificeTokenId4),
      forgeWeaponPureDamage(sacrificeTokenId1, sacrificeTokenId2, sacrificeTokenId3, sacrificeTokenId4),
      false, false);
    encounter.participants.push(msg.sender);

    emit Participating(msg.sender, encounterId);
  }

  function claimLoot(uint encounterId, address player) public whenNotPaused {
    address winnerAddress;
    uint lootTokenId;
    uint consolationPrizeTokenId;
    (winnerAddress, lootTokenId, consolationPrizeTokenId, , ,,) = getEncounterResults(encounterId, player);
    require(winnerAddress == player, "player is not the winner");

    ParticipantData storage participantData = encountersById[encounterId].participantData[player];

    require(!participantData.lootClaimed, "loot already claimed");

    participantData.lootClaimed = true;
    tokenContract.mint(player, lootTokenId);

    // The winner also gets a consolation prize
    // It's possible he called claimConsolationPrizeLoot first, so allow that

    require(consolationPrizeTokenId != 0, "consolation prize invalid");

    if (!participantData.consolationPrizeClaimed) {
        participantData.consolationPrizeClaimed = true;
        // this will throw if the token already exists
        tokenContract.mint(player, consolationPrizeTokenId);

        // refund gas
        msg.sender.transfer(gasRefundForClaimLootWithConsolationPrize);
    } else {
        
        // refund gas
        msg.sender.transfer(gasRefundForClaimLoot);
    }

    emit LootClaimed(player, encounterId);
  }

  function getConsolationPrizeTokenId(uint encounterId, address player) internal view returns (uint) {

    ParticipantData memory participantData = encountersById[encounterId].participantData[player];
    if (participantData.damage == 0) {
      return 0;
    }

    uint blockToHash = getBlockToHashForResults(encounterId);

    uint rand = uint(keccak256(uint(blockhash(blockToHash)) ^ uint(player)));

    if (participantData.forgeWeaponRarity != 0) {
      return constructWeaponTokenId(rand, participantData.forgeWeaponRarity, participantData.forgeWeaponDamagePure);
    }

    return constructWeaponTokenId(rand, 0, 0);
  }

  function claimConsolationPrizeLoot(uint encounterId, address player) public whenNotPaused {
    uint lootTokenId = getConsolationPrizeTokenId(encounterId, player);
    require(lootTokenId != 0, "player didnt participate");

    ParticipantData storage participantData = encountersById[encounterId].participantData[player];
    require(!participantData.consolationPrizeClaimed, "consolation prize already claimed");

    participantData.consolationPrizeClaimed = true;
    tokenContract.mint(player, lootTokenId);

    msg.sender.transfer(gasRefundForClaimConsolationPrizeLoot);

    emit LootClaimed(player, encounterId);
  }

  function balanceOf(address _owner) public view returns (uint256) {
    return tokenContract.balanceOf(_owner);
  }

  function tokensOf(address _owner) public view returns (uint256[]) {
    return tokenContract.tokensOf(_owner);
  }

  function tokenOfOwnerByIndex(address _owner, uint256 _index)
    external
    view
    returns (uint256 _tokenId)
  {
    return tokenContract.tokenOfOwnerByIndex(_owner, _index);
  }
}
function getWeaponDamageFromTokenId(uint tokenId, uint damageType) pure internal returns (uint) {
  return ((tokenId >> (64 + damageType*8)) & 0xff);
}  

// damageType: 0=physical 1=magic 2=water 3=earth 4=fire