contract EtherSpaceCore is ERC721Token, Ownable, Claimable, Destructible {

  string public url = "https://etherspace.co/";

  using SafeMath for uint256;

  struct Spaceship {
    uint16 model;
    bool battleMode;
    uint32 battleWins;
    uint32 battleLosses;
    uint256 battleStake;
    bytes5 upgrades;
    bool isAuction;
    uint256 auctionPrice;
  }

  mapping (uint256 => Spaceship) private spaceships;
  uint256[] private spaceshipIds;

  /* */
  struct SpaceshipProduct {
    uint16 class;
    bytes8 attributes;
    uint256 price; // initial price
    uint32 totalSold; // The quantity of spaceships sold for this model
  }

  mapping (uint16 => SpaceshipProduct) private spaceshipProducts;
  uint16 spaceshipProductCount = 0; // The next count for spaceships products created

  mapping (address => uint256) private balances; // User balances

  // Battle
  uint256 public battleFee = 0;

  // Marketplace
  uint32 public saleFee = 5; // 5%

  EtherSpaceUpgradeInterface public upgradeContract;
  EtherSpaceBattleInterface public battleContract;

  /* Events */
  event EventCashOut (
    address indexed player,
    uint256 amount
  );
  event EventBattleAdd (
    address indexed player,
    uint256 tokenId
  );
  event EventBattleRemove (
    address indexed player,
    uint256 tokenId
  );
  event EventBattle (
    address indexed player,
    uint256 tokenId,
    uint256 tokenIdToAttack,
    uint256 tokenIdWinner
  );
  event EventBuySpaceshipUpgrade (
    address indexed player,
    uint256 tokenId,
    uint16 model,
    uint8 position
  );
  event Log (
    string message
  );

  constructor() public {
    _newSpaceshipProduct(0,   0x001e,   0x0514,   0x0004,   0x0005,   50000000000000000); // price 0.05
    _newSpaceshipProduct(0,   0x001d,   0x0226,   0x0005,   0x0006,   60000000000000000); // price 0.06
    _newSpaceshipProduct(0,   0x001f,   0x03e8,   0x0003,   0x0009,   70000000000000000); // price 0.07
    _newSpaceshipProduct(0,   0x001e,   0x0258,   0x0005,   0x0009,   80000000000000000); // price 0.08
    _newSpaceshipProduct(0,   0x001a,   0x0064,   0x0006,   0x000a,   90000000000000000); // price 0.09
    _newSpaceshipProduct(0,   0x0015,   0x0032,   0x0007,   0x000b,  100000000000000000); // price 0.10
  }

  function _setUpgradeContract(address _address) private {
    EtherSpaceUpgradeInterface candidateContract = EtherSpaceUpgradeInterface(_address);

    require(candidateContract.isEtherSpaceUpgrade());

    // Set the new contract address
    upgradeContract = candidateContract;
  }

  function _setBattleContract(address _address) private {
    EtherSpaceBattleInterface candidateContract = EtherSpaceBattleInterface(_address);

    require(candidateContract.isEtherSpaceBattle());

    // Set the new contract address
    battleContract = candidateContract;
  }

  /* Constructor rejects payments to avoid mistakes */
  function() external payable {
      require(false);
  }

  /* ERC721Metadata */
  function name() external pure returns (string) {
    return "EtherSpace";
  }

  function symbol() external pure returns (string) {
    return "ESPC";
  }

  /* Enable listing of all deeds (alternative to ERC721Enumerable to avoid having to work with arrays). */
  function ids() external view returns (uint256[]) {
    return spaceshipIds;
  }

  /* Owner functions */
  function setSpaceshipPrice(uint16 _model, uint256 _price) external onlyOwner {
    require(_price > 0);

    spaceshipProducts[_model].price = _price;
  }

  function newSpaceshipProduct(uint16 _class, bytes2 _propulsion, bytes2 _weight, bytes2 _attack, bytes2 _armour, uint256 _price) external onlyOwner {
    _newSpaceshipProduct(_class, _propulsion, _weight, _attack, _armour, _price);
  }

  function setBattleFee(uint256 _fee) external onlyOwner {
    battleFee = _fee;
  }

  function setUpgradeContract(address _address) external onlyOwner {
    _setUpgradeContract(_address);
  }

  function setBattleContract(address _address) external onlyOwner {
    _setBattleContract(_address);
  }

  function giftSpaceship(uint16 _model, address _player) external onlyOwner {
    _generateSpaceship(_model, _player);
  }

  function newSpaceshipUpgrade(bytes1 _identifier, uint8 _position, uint256 _price) external onlyOwner {
    upgradeContract.newSpaceshipUpgrade(_identifier, _position, _price);
  }

  /* Spaceship Product functions */
  function _newSpaceshipProduct(uint16 _class, bytes2 _propulsion, bytes2 _weight, bytes2 _attack, bytes2 _armour, uint256 _price) private {
    bytes memory attributes = new bytes(8);
    attributes[0] = _propulsion[0];
    attributes[1] = _propulsion[1];
    attributes[2] = _weight[0];
    attributes[3] = _weight[1];
    attributes[4] = _attack[0];
    attributes[5] = _attack[1];
    attributes[6] = _armour[0];
    attributes[7] = _armour[1];

    spaceshipProducts[spaceshipProductCount++] = SpaceshipProduct(_class, ByteConvert.bytesToBytes8(attributes), _price, 0);
  }

  /* CashOut */
  function cashOut() public {
    require(address(this).balance >= balances[msg.sender]); // Checking if this contract has enought money to pay
    require(balances[msg.sender] > 0); // Cannot cashOut zero amount

    uint256 _balance = balances[msg.sender];

    balances[msg.sender] = 0;
    msg.sender.transfer(_balance);

    emit EventCashOut(msg.sender, _balance);
  }

  /* Marketplace functions */
  function buySpaceship(uint16 _model) public payable {
    require(msg.value > 0);
    require(msg.value == spaceshipProducts[_model].price);
    require(spaceshipProducts[_model].price > 0);

    _generateSpaceship(_model, msg.sender);

    balances[owner] += spaceshipProducts[_model].price;
  }

  function _generateSpaceship(uint16 _model, address _player) private {
    // Build a new spaceship for player
    uint256 tokenId = spaceshipIds.length;
    spaceshipIds.push(tokenId);
    super._mint(_player, tokenId);

    spaceships[tokenId] = Spaceship({
      model: _model,
      battleMode: false,
      battleWins: 0,
      battleLosses: 0,
      battleStake: 0,
      upgrades: "\x00\x00\x00\x00\x00", // Prepared to have 5 different types of upgrades
      isAuction: false,
      auctionPrice: 0
    });

    spaceshipProducts[_model].totalSold++;
  }

  function sellSpaceship(uint256 _tokenId, uint256 _price) public onlyOwnerOf(_tokenId) {
    spaceships[_tokenId].isAuction = true;
    spaceships[_tokenId].auctionPrice = _price;
  }

  function bidSpaceship(uint256 _tokenId) public payable {
    require(getPlayerSpaceshipAuctionById(_tokenId)); // must be for sale
    require(getPlayerSpaceshipAuctionPriceById(_tokenId) == msg.value); // value must be exactly

    // Giving the sold percentage fee to contract owner
    uint256 ownerPercentage = msg.value.mul(uint256(saleFee)).div(100);
    balances[owner] += ownerPercentage;

    // Giving the sold amount minus owner fee to seller
    balances[getPlayerSpaceshipOwnerById(_tokenId)] += msg.value.sub(ownerPercentage);

    // Transfering spaceship to buyer
    super.clearApprovalAndTransfer(getPlayerSpaceshipOwnerById(_tokenId), msg.sender, _tokenId);

    // Removing from auction
    spaceships[_tokenId].isAuction = false;
    spaceships[_tokenId].auctionPrice = 0;
  }

  /* Battle functions */
  function battleAdd(uint256 _tokenId) public payable onlyOwnerOf(_tokenId) {
    require(msg.value == getPlayerSpaceshipBattleStakeById(_tokenId));
    require(msg.value > 0);
    require(spaceships[_tokenId].battleMode == false);

    spaceships[_tokenId].battleMode = true;
    spaceships[_tokenId].battleStake = msg.value;

    emit EventBattleAdd(msg.sender, _tokenId);
  }

  function battleRemove(uint256 _tokenId) public onlyOwnerOf(_tokenId) {
    require(spaceships[_tokenId].battleMode == true);

    spaceships[_tokenId].battleMode = false;

    balances[msg.sender] = balances[msg.sender].add(spaceships[_tokenId].battleStake);

    emit EventBattleRemove(msg.sender, _tokenId);
  }

  function battle(uint256 _tokenId, uint256 _tokenIdToAttack) public payable onlyOwnerOf(_tokenId) {
    require (spaceships[_tokenIdToAttack].battleMode == true); // ship to attack must be in battle mode
    require (spaceships[_tokenId].battleMode == false); // attacking ship must not be offered for battle
    require(msg.value == getPlayerSpaceshipBattleStakeById(_tokenId));

    uint256 battleStakeDefender = spaceships[_tokenIdToAttack].battleStake;

    bool result = battleContract.battle(spaceshipProducts[spaceships[_tokenId].model].attributes, spaceships[_tokenId].upgrades, spaceshipProducts[spaceships[_tokenIdToAttack].model].attributes, spaceships[_tokenIdToAttack].upgrades);

    if (result) {
        spaceships[_tokenId].battleWins++;
        spaceships[_tokenIdToAttack].battleLosses++;

        balances[super.ownerOf(_tokenId)] += (battleStakeDefender + msg.value) - battleFee;
        spaceships[_tokenIdToAttack].battleStake = 0;

        emit EventBattle(msg.sender, _tokenId, _tokenIdToAttack, _tokenId);

    } else {
        spaceships[_tokenId].battleLosses++;
        spaceships[_tokenIdToAttack].battleWins++;

        balances[super.ownerOf(_tokenIdToAttack)] += (battleStakeDefender + msg.value) - battleFee;
        spaceships[_tokenIdToAttack].battleStake = 0;

        emit EventBattle(msg.sender, _tokenId, _tokenIdToAttack, _tokenIdToAttack);
    }

    balances[owner] += battleFee;

    spaceships[_tokenIdToAttack].battleMode = false;
  }

  /* Upgrade functions */
  function buySpaceshipUpgrade(uint256 _tokenId, uint16 _model, uint8 _position) public payable onlyOwnerOf(_tokenId) {
    require(msg.value > 0);
    uint256 upgradePrice = upgradeContract.getSpaceshipUpgradePriceByModel(_model, _position);
    require(msg.value == upgradePrice);
    require(getPlayerSpaceshipBattleModeById(_tokenId) == false);

    bytes5 currentUpgrades = spaceships[_tokenId].upgrades;
    upgradeContract.isSpaceshipUpgradeAllowed(currentUpgrades, _model, _position);

    spaceships[_tokenId].upgrades = upgradeContract.buySpaceshipUpgrade(currentUpgrades, _model, _position);

    balances[owner] += upgradePrice;

    emit EventBuySpaceshipUpgrade(msg.sender, _tokenId, _model, _position);
  }

  /* Getters getPlayer* */
  function getPlayerSpaceshipCount(address _player) public view returns (uint256) {
    return super.balanceOf(_player);
  }

  function getPlayerSpaceshipModelById(uint256 _tokenId) public view returns (uint16) {
    return spaceships[_tokenId].model;
  }

  function getPlayerSpaceshipOwnerById(uint256 _tokenId) public view returns (address) {
    return super.ownerOf(_tokenId);
  }

  function getPlayerSpaceshipModelByIndex(address _owner, uint256 _index) public view returns (uint16) {
    return spaceships[super.tokensOf(_owner)[_index]].model;
  }

  function getPlayerSpaceshipAuctionById(uint256 _tokenId) public view returns (bool) {
    return spaceships[_tokenId].isAuction;
  }

  function getPlayerSpaceshipAuctionPriceById(uint256 _tokenId) public view returns (uint256) {
    return spaceships[_tokenId].auctionPrice;
  }

  function getPlayerSpaceshipBattleModeById(uint256 _tokenId) public view returns (bool) {
    return spaceships[_tokenId].battleMode;
  }

  function getPlayerSpaceshipBattleStakePaidById(uint256 _tokenId) public view returns (uint256) {
    return spaceships[_tokenId].battleStake;
  }

  function getPlayerSpaceshipBattleStakeById(uint256 _tokenId) public view returns (uint256) {
    return battleContract.calculateStake(spaceshipProducts[spaceships[_tokenId].model].attributes, spaceships[_tokenId].upgrades);
  }

  function getPlayerSpaceshipBattleLevelById(uint256 _tokenId) public view returns (uint256) {
    return battleContract.calculateLevel(spaceshipProducts[spaceships[_tokenId].model].attributes, spaceships[_tokenId].upgrades);
  }

  function getPlayerSpaceshipBattleWinsById(uint256 _tokenId) public view returns (uint32) {
    return spaceships[_tokenId].battleWins;
  }

  function getPlayerSpaceshipBattleLossesById(uint256 _tokenId) public view returns (uint32) {
    return spaceships[_tokenId].battleLosses;
  }

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

  function getPlayerBalance(address _owner) public view returns (uint256) {
    return balances[_owner];
  }

  function getPlayerSpaceshipUpgradesById(uint256 _tokenId) public view returns (bytes5) {
    return spaceships[_tokenId].upgrades;
  }

  /* Getters getSpaceshipProduct* */
  function getSpaceshipProductPriceByModel(uint16 _model) public view returns (uint256) {
    return spaceshipProducts[_model].price;
  }

  function getSpaceshipProductClassByModel(uint16 _model) public view returns (uint16) {
    return spaceshipProducts[_model].class;
  }

  function getSpaceshipProductTotalSoldByModel(uint16 _model) public view returns (uint256) {
    return spaceshipProducts[_model].totalSold;
  }

  function getSpaceshipProductAttributesByModel(uint16 _model) public view returns (bytes8) {
    return spaceshipProducts[_model].attributes;
  }

  function getSpaceshipProductCount() public view returns (uint16) {
    return spaceshipProductCount;
  }

  /* Getters getSpaceship* */
  function getSpaceshipTotalSold() public view returns (uint256) {
    return super.totalSupply();
  }

  /* Getters Spaceship Upgrades */
  function getSpaceshipUpgradePriceByModel(uint16 _model, uint8 _position) public view returns (uint256) {
    return upgradeContract.getSpaceshipUpgradePriceByModel(_model, _position);
  }

  function getSpaceshipUpgradeTotalSoldByModel(uint16 _model, uint8 _position) public view returns (uint256) {
    return upgradeContract.getSpaceshipUpgradeTotalSoldByModel(_model, _position);
  }

  function getSpaceshipUpgradeCount() public view returns (uint256) {
    return upgradeContract.getSpaceshipUpgradeCount();
  }

}
function() external payable {
    require(false);
}

/* Constructor rejects payments to avoid mistakes */