contract Traits is Ownable, ITraits {
  using Strings for uint256;

  // struct to store each trait's data for metadata and rendering
  struct Trait {
    string name;
    string png;
  }

  // mapping from trait type (index) to its name
  string[2] private _traitTypes = [
    "Base",
    "Accessory"
  ];
  // storage of each traits name and base64 PNG data
  mapping(uint8 => mapping(uint8 => Trait)) public traitData;

  ISharks public sharksNft;

  constructor() {}

  /** ADMIN */

  function setSharks(address _sharksNft) external onlyOwner {
    sharksNft = ISharks(_sharksNft);
  }

  /**
   * administrative to upload the names and images associated with each trait
   * @param traitType the trait type to upload the traits for (see traitTypes for a mapping)
   * @param traits the names and base64 encoded PNGs for each trait
   */
  function uploadTraits(uint8 traitType, uint8[] calldata traitIds, Trait[] calldata traits) external onlyOwner {
    require(traitIds.length == traits.length, "Mismatched inputs");
    for (uint i = 0; i < traits.length; i++) {
      traitData[traitType][traitIds[i]] = Trait(
        traits[i].name,
        traits[i].png
      );
    }
  }

  /** RENDER */

  /**
   * generates an <image> element using base64 encoded PNGs
   * @param trait the trait storing the PNG data
   * @return the <image> element
   */
  function drawTrait(Trait memory trait) internal pure returns (string memory) {
    return string(abi.encodePacked(
      '<image x="0" y="0" width="400" height="400" image-rendering="pixelated" preserveAspectRatio="xMidYMid" xlink:href="',
      trait.png,
      '"/>'
    ));
  }

  /**
   * generates an entire SVG by composing multiple <image> elements of PNGs
   * @param tokenId the ID of the token to generate an SVG for
   * @return a valid SVG of the Wizard / Dragon
   */
  function drawSVG(uint256 tokenId) internal view returns (string memory) {
    ISharks.SGToken memory s = sharksNft.getTokenTraits(tokenId);
    uint8 shift = uint8(s.tokenType) * 2;

    string memory svgString = string(abi.encodePacked(
      drawTrait(traitData[0 + shift][s.base]),
      drawTrait(traitData[1 + shift][s.accessory])
    ));

    return string(abi.encodePacked(
      '<svg id="sharksNft" width="100%" height="100%" version="1.1" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">',
      '<image x="0" y="0" width="400" height="400" image-rendering="pixelated" preserveAspectRatio="xMidYMid" xlink:href="https://cdn.sharkgame.app/shark/bg/underwater.png"/>',
      svgString,
      "</svg>"
    ));
  }

  /**
   * generates an attribute for the attributes array in the ERC721 metadata standard
   * @param traitType the trait type to reference as the metadata key
   * @param value the token's trait associated with the key
   * @return a JSON dictionary for the single attribute
   */
  function attributeForTypeAndValue(string memory traitType, string memory value) internal pure returns (string memory) {
    return string(abi.encodePacked(
      '{"trait_type":"',
      traitType,
      '","value":"',
      value,
      '"}'
    ));
  }

  function typeToString(ISharks.SGTokenType tokenType) internal pure returns (string memory) {
    return tokenType == ISharks.SGTokenType.MINNOW ? "Minnow" : (tokenType == ISharks.SGTokenType.SHARK ? "Shark" : "Orca");
  }

  /**
   * generates an array composed of all the individual traits and values
   * @param tokenId the ID of the token to compose the metadata for
   * @return a JSON array of all of the attributes for given token ID
   */
  function compileAttributes(uint256 tokenId) internal view returns (string memory) {
    ISharks.SGToken memory s = sharksNft.getTokenTraits(tokenId);
    uint8 shift = uint8(s.tokenType) * 2;
    string memory traits;
    traits = string(abi.encodePacked(
      attributeForTypeAndValue(_traitTypes[0], traitData[0 + shift][s.base].name),',',
      attributeForTypeAndValue(_traitTypes[1], traitData[1 + shift][s.accessory].name),','
    ));
    uint256 gen = tokenId <= 5000 ? 0 : (tokenId <= 10000 ? 1 : (tokenId <= 20000 ? 2 : 3));
    return string(abi.encodePacked(
      '[',
      traits,
      '{"trait_type":"Generation","value":"Gen ',gen.toString(),
      '"},{"trait_type":"Type","value":"',
      typeToString(s.tokenType),
      '"}]'
    ));
  }

  /**
   * generates a base64 encoded metadata response without referencing off-chain content
   * @param tokenId the ID of the token to generate the metadata for
   * @return a base64 encoded JSON dictionary of the token's metadata and SVG
   */
  function tokenURI(uint256 tokenId) public view override returns (string memory) {
    require(_msgSender() == address(sharksNft), "hmmmm what doing?");
    ISharks.SGToken memory s = sharksNft.getTokenTraits(tokenId);

    string memory metadata = string(abi.encodePacked(
      '{"name": "',
      typeToString(s.tokenType)," #",
      tokenId.toString(),
      '", "description": "Shark Game is a gamified NFT protocol with next-level tokenomics, featuring first-class security measures, truly random game mechanics, and a robust user interface.", "image": "data:image/svg+xml;base64,',
      base64(bytes(drawSVG(tokenId))),
      '", "attributes":',
      compileAttributes(tokenId),
      "}"
    ));

    return string(abi.encodePacked(
      "data:application/json;base64,",
      base64(bytes(metadata))
    ));
  }

  /** BASE 64 - Written by Brech Devos */

  string internal constant TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

  function base64(bytes memory data) internal pure returns (string memory) {
    if (data.length == 0) return '';

    // load the table into memory
    string memory table = TABLE;

    // multiply by 4/3 rounded up
    uint256 encodedLen = 4 * ((data.length + 2) / 3);

    // add some extra buffer at the end required for the writing
    string memory result = new string(encodedLen + 32);

    assembly {
      // set the actual output length
      mstore(result, encodedLen)

      // prepare the lookup table
      let tablePtr := add(table, 1)

      // input ptr
      let dataPtr := data
      let endPtr := add(dataPtr, mload(data))

      // result ptr, jump over length
      let resultPtr := add(result, 32)

      // run over the input, 3 bytes at a time
      for {} lt(dataPtr, endPtr) {}
      {
          dataPtr := add(dataPtr, 3)

          // read 3 bytes
          let input := mload(dataPtr)

          // write 4 characters
          mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(18, input), 0x3F)))))
          resultPtr := add(resultPtr, 1)
          mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(12, input), 0x3F)))))
          resultPtr := add(resultPtr, 1)
          mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr( 6, input), 0x3F)))))
          resultPtr := add(resultPtr, 1)
          mstore(resultPtr, shl(248, mload(add(tablePtr, and(        input,  0x3F)))))
          resultPtr := add(resultPtr, 1)
      }

      // padding with '='
      switch mod(mload(data), 3)
      case 1 { mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) }
      case 2 { mstore(sub(resultPtr, 1), shl(248, 0x3d)) }
    }

    return result;
  }
}
function drawTrait(Trait memory trait) internal pure returns (string memory) {
  return string(abi.encodePacked(
    '<image x="0" y="0" width="400" height="400" image-rendering="pixelated" preserveAspectRatio="xMidYMid" xlink:href="',
    trait.png,
    '"/>'
  ));
}

/**
 * generates an <image> element using base64 encoded PNGs
 * @param trait the trait storing the PNG data
 * @return the <image> element
 */