contract PetCemetheryToken is ERC721Token, Ownable {

  using SafeMath for *;

  enum TokenType { Default, Plot, Headstone, Decoration, PhotoVoucher }

  struct PlotPosition {
    uint32 section;
    uint8 index;
  }

  struct Plot {
    uint256 headstoneTokenId;
    bool forSale;
    uint256 price;
    uint256 photoExpirationTime;
  }

  struct Headstone {
    uint16 headstoneType;
    uint256 plotTokenId;
    string engraving;
    string petName;
    uint16 petSpecies;
    uint32 lat;
    uint32 lng;
    string extraData;
    bool flipped;
  }

  struct Decoration {
    uint16 decorationType;
    uint256 plotTokenId;
    int16 plotPositionX;
    int16 plotPositionY;
    bool flipped;
    uint256 firstAssignTime;
    uint256 latestAssignTime;
    uint256 totalAssignDuration;
  }

  struct PhotoVoucher {
    uint256 period;
  }

  struct PlotDetailsView {
    uint256 plotTokenId;
    Plot plot;
    PlotPosition plotPosition;
    Headstone headstone;
    DecorationDetailsView[] decorations;
    address owner;
    string tokenUri;
  }

  struct DecorationDetailsView {
    uint256 decorationTokenId;
    Decoration decoration;
  }

  string public baseURI;

  uint32 public sectionCounter;
  uint256 public tokenIdCounter = 1;

  mapping(uint32 => uint256[]) public plotIdByPosition; // section - index -> plotTokenId

  mapping(uint256 => TokenType) public tokenTypes;

  mapping(uint256 => Plot) public plots;
  mapping(uint256 => Headstone) public headstones;
  mapping(uint256 => Decoration) public decorations;
  mapping(uint256 => PhotoVoucher) public photoVouchers;
  mapping(uint256 => uint256[]) public plotsDecorations; // plot id -> decoration ids
  mapping(uint256 => PlotPosition) public plotsPositions;

  constructor(string _baseURI) public ERC721Token("Pet Cemethery Token", "PCT") {
    baseURI = _baseURI;
  }

  function _mintSection(uint8 _size) private {
    uint32 section = sectionCounter;
    sectionCounter++;

    plotIdByPosition[section] = new uint256[](_size);

    for (uint8 i = 0; i < _size; i++) {
      _mintPlot(section, i);
    }
  }

  function _mintPlot(uint32 _section, uint8 _index) private {
    uint256 newTokenId = _getNextTokenId();
    _mint(owner, newTokenId);
    plots[newTokenId] = Plot(0, false, 0, 0);
    plotsDecorations[newTokenId] = new uint256[](0);
    plotsPositions[newTokenId] = PlotPosition(_section, _index);
    tokenTypes[newTokenId] = TokenType.Plot;
    plotIdByPosition[_section][_index] = newTokenId;
  }

  function _mintHeadstone(uint16 _headstoneType) private {
    uint256 newTokenId = _getNextTokenId();
    _mint(owner, newTokenId);
    headstones[newTokenId] = Headstone(_headstoneType, 0, "", "", 0, 0, 0, "", false);
    tokenTypes[newTokenId] = TokenType.Headstone;
  }

  function _mintDecoration(uint16 _decorationType) private {
    uint256 newTokenId = _getNextTokenId();
    _mint(owner, newTokenId);
    decorations[newTokenId] = Decoration(_decorationType, 0, 0, 0, false, 0, 0, 0);
    tokenTypes[newTokenId] = TokenType.Decoration;
  }

  function _mintPhotoVoucher(uint256 _period) private {
    uint256 newTokenId = _getNextTokenId();
    _mint(owner, newTokenId);
    photoVouchers[newTokenId] = PhotoVoucher(_period);
    tokenTypes[newTokenId] = TokenType.PhotoVoucher;
  }

  function _getNextTokenId() private returns (uint256) {
    uint256 res = tokenIdCounter;
    tokenIdCounter = tokenIdCounter.add(1);
    return res;
  }

  function _assignHeadstoneToPlot(uint256 _plotTokenId, uint256 _headstoneTokenId) private {
    _unassignHeadstoneFromPlot(_plotTokenId);
    plots[_plotTokenId].headstoneTokenId = _headstoneTokenId;
    headstones[_headstoneTokenId].plotTokenId = _plotTokenId;
  }

  function _unassignHeadstoneFromPlot(uint256 _plotTokenId) private {
    uint256 existingHeadstoneTokenId = plots[_plotTokenId].headstoneTokenId;

    if (existingHeadstoneTokenId != 0) {
      headstones[existingHeadstoneTokenId].plotTokenId = 0;
      headstones[existingHeadstoneTokenId].engraving = "";
      headstones[existingHeadstoneTokenId].petName = "";
      headstones[existingHeadstoneTokenId].petSpecies = 0;
      headstones[existingHeadstoneTokenId].lat = 0;
      headstones[existingHeadstoneTokenId].lng = 0;
      headstones[existingHeadstoneTokenId].extraData = "";
      headstones[existingHeadstoneTokenId].flipped = false;
    }

    plots[_plotTokenId].headstoneTokenId = 0;
  }

  function _assignDecorationToPlot(uint256 _plotTokenId, uint256 _decorationTokenId,
                                   int16 _plotPositionX, int16 _plotPositionY, bool _flipped) private {
    if (decorations[_decorationTokenId].plotTokenId != _plotTokenId) {
      plotsDecorations[_plotTokenId].push(_decorationTokenId);
    }
    decorations[_decorationTokenId].plotTokenId = _plotTokenId;
    decorations[_decorationTokenId].plotPositionX = _plotPositionX;
    decorations[_decorationTokenId].plotPositionY = _plotPositionY;
    decorations[_decorationTokenId].flipped = _flipped;
    if (decorations[_decorationTokenId].firstAssignTime == 0) {
      decorations[_decorationTokenId].firstAssignTime = now;
    }
    decorations[_decorationTokenId].latestAssignTime = now;
  }

  function _unassignDecorationFromPlot(uint256 _plotTokenId, uint256 _decorationTokenId) private {
    Decoration storage decoration = decorations[_decorationTokenId];
    if (decoration.plotTokenId != 0) {
      uint256 latestAssignDuration = now - decoration.latestAssignTime;
      decoration.totalAssignDuration = decoration.totalAssignDuration.add(latestAssignDuration);
    }
    decoration.plotTokenId = 0;
    decoration.plotPositionX = 0;
    decoration.plotPositionY = 0;
    decoration.flipped = false;

    for (uint256 i = 0; i < plotsDecorations[_plotTokenId].length; i++) {
      if (plotsDecorations[_plotTokenId][i] == _decorationTokenId) {
        delete plotsDecorations[_plotTokenId][i];

        if (i != plotsDecorations[_plotTokenId].length - 1) {
          plotsDecorations[_plotTokenId][i] = plotsDecorations[_plotTokenId][plotsDecorations[_plotTokenId].length-1];
        }

        plotsDecorations[_plotTokenId].length--;
        break;
      }
    }
  }

  function _unassignAllDecorationsFromPlot(uint256 _plotTokenId) private {
    for (uint256 i = 0; i < plotsDecorations[_plotTokenId].length; i++) {
      Decoration storage decoration = decorations[plotsDecorations[_plotTokenId][i]];
      decoration.plotTokenId = 0;
      decoration.plotPositionX = 0;
      decoration.plotPositionY = 0;
      decoration.flipped = false;

      uint256 latestAssignDuration = now - decoration.latestAssignTime;
      decoration.totalAssignDuration = decoration.totalAssignDuration.add(latestAssignDuration);
    }
    delete plotsDecorations[_plotTokenId];
  }

  function _offerPlot(uint256 _plotTokenId, uint256 _price) private {
    _unassignHeadstoneFromPlot(_plotTokenId);
    _unassignAllDecorationsFromPlot(_plotTokenId);

    plots[_plotTokenId].forSale = true;
    plots[_plotTokenId].price = _price;
  }

  function _cancelPlotOffer(uint256 _plotTokenId) private {
    plots[_plotTokenId].forSale = false;
    plots[_plotTokenId].price = 0;
  }

  /* Public methods - getters (views) */

  function getOwnedTokens(address _address) public view returns (uint256[]) {
    return ownedTokens[_address];
  }

  function getPlotDetails(uint256 _plotTokenId) public view returns (PlotDetailsView memory) {
    Plot storage plot = plots[_plotTokenId];
    PlotPosition storage plotPosition = plotsPositions[_plotTokenId];
    Headstone storage headstone = headstones[plot.headstoneTokenId];
    uint256[] storage decorationsIds = plotsDecorations[_plotTokenId];
    DecorationDetailsView[] memory retDecorations = new DecorationDetailsView[](decorationsIds.length);
    for (uint256 j = 0; j < decorationsIds.length; j++) {
      retDecorations[j] = DecorationDetailsView(decorationsIds[j], decorations[decorationsIds[j]]);
    }
    address owner = ownerOf(_plotTokenId);
    string memory tokenUri = tokenURI(_plotTokenId);
    return PlotDetailsView(_plotTokenId, plot, plotPosition, headstone, retDecorations, owner, tokenUri);
  }

  function getSectionPlotsDetails(uint32 _section) public view returns (PlotDetailsView[] memory) {
    uint256[] storage plotsIds = plotIdByPosition[_section];
    PlotDetailsView[] memory plotsDetailsViews = new PlotDetailsView[](plotsIds.length);
    for (uint256 i = 0; i<plotsIds.length; i++) {
      plotsDetailsViews[i] = getPlotDetails(plotsIds[i]);
    }

    return plotsDetailsViews;
  }

  function plotsCount(uint32 _section) public view returns (uint256) {
    return plotIdByPosition[_section].length;
  }

  function photoExpirationTime(uint256 _plotTokenId) public view returns (uint256) {
    return plots[_plotTokenId].photoExpirationTime;
  }

  /* Public methods - plot, headstone, decorations management */

  function prepareHeadstoneAndAssignToPlot(uint256 _plotTokenId, uint256 _headstoneTokenId,
                                           string _engraving, string _petName, uint16 _petSpecies,
                                           uint32 _lat, uint32 _lng, string _extraData, bool _flipped) public {
    prepareHeadstone(_headstoneTokenId, _engraving, _petName, _petSpecies, _lat, _lng, _extraData, _flipped);
    assignHeadstoneToPlot(_plotTokenId, _headstoneTokenId);
  }

  function prepareHeadstone(uint256 _headstoneTokenId, string _engraving,
                            string _petName, uint16 _petSpecies, uint32 _lat, uint32 _lng, string _extraData, bool _flipped) public onlyOwnerOf(_headstoneTokenId) {
    headstones[_headstoneTokenId].engraving = _engraving;
    headstones[_headstoneTokenId].petName = _petName;
    headstones[_headstoneTokenId].petSpecies = _petSpecies;
    headstones[_headstoneTokenId].lat = _lat;
    headstones[_headstoneTokenId].lng = _lng;
    headstones[_headstoneTokenId].extraData = _extraData;
    headstones[_headstoneTokenId].flipped = _flipped;
  }

  function assignHeadstoneToPlot(uint256 _plotTokenId,
                                 uint256 _headstoneTokenId) public onlyOwnerOf(_plotTokenId) onlyOwnerOf(_headstoneTokenId) {
    require(tokenTypes[_plotTokenId] == TokenType.Plot, "Invalid plot token ID");
    require(tokenTypes[_headstoneTokenId] == TokenType.Headstone, "Invalid headstone token ID");
    require(!plots[_plotTokenId].forSale, "Plot is offered for sale");

    _assignHeadstoneToPlot(_plotTokenId, _headstoneTokenId);
  }

  function unassignHeadstoneFromPlot(uint256 _plotTokenId) public onlyOwnerOf(_plotTokenId) {
    require(tokenTypes[_plotTokenId] == TokenType.Plot, "Invalid plot token ID");

    _unassignHeadstoneFromPlot(_plotTokenId);
  }

  function assignDecorationToPlot(uint256 _plotTokenId, uint256 _decorationTokenId,
                                  int16 _plotPositionX, int16 _plotPositionY, bool _flipped) public onlyOwnerOf(_plotTokenId) onlyOwnerOf(_decorationTokenId) {
    require(tokenTypes[_plotTokenId] == TokenType.Plot, "Invalid plot token ID");
    require(tokenTypes[_decorationTokenId] == TokenType.Decoration, "Invalid decoration token ID");
    require(!plots[_plotTokenId].forSale, "Plot is offered for sale");

    _assignDecorationToPlot(_plotTokenId, _decorationTokenId, _plotPositionX, _plotPositionY, _flipped);
  }

  function unassignDecorationFromPlot(uint256 _plotTokenId, uint256 _decorationTokenId) public onlyOwnerOf(_plotTokenId) {
    require(tokenTypes[_plotTokenId] == TokenType.Plot, "Invalid plot token ID");
    require(tokenTypes[_decorationTokenId] == TokenType.Decoration, "Invalid decoration token ID");

    _unassignDecorationFromPlot(_plotTokenId, _decorationTokenId);
  }

  /* Public methods - plot trading */

  function offerPlot(uint256 _plotTokenId, uint256 _price) public onlyOwnerOf(_plotTokenId) {
    require(tokenTypes[_plotTokenId] == TokenType.Plot, "Invalid plot token ID");

    _offerPlot(_plotTokenId, _price);
  }

  function batchOfferPlots(uint256[] _plotsTokensIds, uint256 _price) public {
    for (uint256 i = 0; i < _plotsTokensIds.length; i++) {
      offerPlot(_plotsTokensIds[i], _price);
    }
  }

  function cancelPlotOffer(uint256 _plotTokenId) public onlyOwnerOf(_plotTokenId) {
    require(plots[_plotTokenId].forSale, "Plot is offered for sale");

    _cancelPlotOffer(_plotTokenId);
  }

  function buyPlot(uint256 _plotTokenId) public payable {
    require(tokenTypes[_plotTokenId] == TokenType.Plot, "Invalid plot token ID");

    address plotOwner = ownerOf(_plotTokenId);
    uint256 price = plots[_plotTokenId].price;

    require(plots[_plotTokenId].forSale, "Plot is offered for sale");
    require(msg.value == price, "Invalid tx value");
    require(msg.sender != plotOwner, "Buyer is owner");

    tokenApprovals[_plotTokenId] = msg.sender;
    safeTransferFrom(plotOwner, msg.sender, _plotTokenId);

    plotOwner.transfer(msg.value);
  }

  /* Public methods - photo voucher */

  function redeemPhotoVoucher(uint256 _photoVoucherTokenId,
                              uint256 _plotTokenId) public onlyOwnerOf(_photoVoucherTokenId) onlyOwnerOf(_plotTokenId) {
    require(tokenTypes[_photoVoucherTokenId] == TokenType.PhotoVoucher, "Invalid photo voucher token ID");
    require(tokenTypes[_plotTokenId] == TokenType.Plot, "Invalid plot token ID");

    Plot storage plot = plots[_plotTokenId];
    if (plot.photoExpirationTime > now) {
      plot.photoExpirationTime = plot.photoExpirationTime.add(photoVouchers[_photoVoucherTokenId].period);
    } else {
      plot.photoExpirationTime = now.add(photoVouchers[_photoVoucherTokenId].period);
    }

    _burn(msg.sender, _photoVoucherTokenId);
    delete photoVouchers[_photoVoucherTokenId];
    delete tokenTypes[_photoVoucherTokenId];
  }

  /* Owner only */

  function mintSections(uint8 _num, uint8 _size) public onlyOwner {
    for (uint8 i = 0; i < _num; i++) {
      _mintSection(_size);
    }
  }

  function mintHeadstones(uint8 _num, uint16 _headstoneType) public onlyOwner {
    for (uint8 i = 0; i < _num; i++) {
      _mintHeadstone(_headstoneType);
    }
  }

  function mintDecorations(uint8 _num, uint16 _decorationType) public onlyOwner {
    for (uint8 i = 0; i < _num; i++) {
      _mintDecoration(_decorationType);
    }
  }

  function mintPhotoVouchers(uint8 _num, uint256 _period) public onlyOwner {
    for (uint8 i = 0; i < _num; i++) {
      _mintPhotoVoucher(_period);
    }
  }

  function setBaseURI(string _baseURI) public onlyOwner {
    baseURI = _baseURI;
  }

  /* Override methods */

  function transferFrom(address _from, address _to, uint256 _tokenId) public {
    ERC721BasicToken.transferFrom(_from, _to, _tokenId);

    if (tokenTypes[_tokenId] == TokenType.Plot) {
      _cancelPlotOffer(_tokenId);
      _unassignHeadstoneFromPlot(_tokenId);
      _unassignAllDecorationsFromPlot(_tokenId);
    } else if (tokenTypes[_tokenId] == TokenType.Headstone) {
      if (headstones[_tokenId].plotTokenId != 0) {
        _unassignHeadstoneFromPlot(headstones[_tokenId].plotTokenId);
      }
    } else if (tokenTypes[_tokenId] == TokenType.Decoration) {
      if (decorations[_tokenId].plotTokenId != 0) {
        _unassignDecorationFromPlot(decorations[_tokenId].plotTokenId, _tokenId);
      }
    } // else if PhotoVoucher: noop
  }

  function tokenURI(uint256 _tokenId) public view returns (string) {
    return strConcat(baseURI, strConcat("/token/", uint2str(_tokenId)));
  }

  /* Utils */

  function strConcat(string _a, string _b) internal pure returns (string) {
    bytes memory _ba = bytes(_a);
    bytes memory _bb = bytes(_b);
    string memory ab = new string(_ba.length + _bb.length);
    bytes memory ba = bytes(ab);
    uint k = 0;
    for (uint i = 0; i < _ba.length; i++) ba[k++] = _ba[i];
    for (i = 0; i < _bb.length; i++) ba[k++] = _bb[i];
    return string(ba);
  }

  function uint2str(uint256 _i) internal pure returns (string) {
    if (_i == 0) return "0";
    uint j = _i;
    uint length;
    while (j != 0){
      length++;
      j /= 10;
    }
    bytes memory bstr = new bytes(length);
    uint k = length - 1;
    while (_i != 0){
      bstr[k--] = byte(48 + _i % 10);
      _i /= 10;
    }
    return string(bstr);
  }
}
function redeemPhotoVoucher(uint256 _photoVoucherTokenId,
                            uint256 _plotTokenId) public onlyOwnerOf(_photoVoucherTokenId) onlyOwnerOf(_plotTokenId) {
  require(tokenTypes[_photoVoucherTokenId] == TokenType.PhotoVoucher, "Invalid photo voucher token ID");
  require(tokenTypes[_plotTokenId] == TokenType.Plot, "Invalid plot token ID");

  Plot storage plot = plots[_plotTokenId];
  if (plot.photoExpirationTime > now) {
    plot.photoExpirationTime = plot.photoExpirationTime.add(photoVouchers[_photoVoucherTokenId].period);
  } else {
    plot.photoExpirationTime = now.add(photoVouchers[_photoVoucherTokenId].period);
  }

  _burn(msg.sender, _photoVoucherTokenId);
  delete photoVouchers[_photoVoucherTokenId];
  delete tokenTypes[_photoVoucherTokenId];
}

/* Public methods - photo voucher */