// File: /contracts/Husk.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.4.24;

// import './ReadsAzimuth.sol';

// import "openzeppelin-solidity/contracts/token/ERC721/IERC721Receiver.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
// import "openzeppelin-solidity/contracts/access/AccessControl.sol";
import "openzeppelin-solidity/contracts/access/rbac/RBAC.sol";

// azimuth address: 0x33EeCbf908478C10614626A9D304bfe18B78DD73
// azimuth goerli:  0xbB61Fa683E4B910418E27b00a1438a936234df52
// keccak256 encoded "SHIP_ADDER_ROLE": 0x381e9228496910d7a11e0b24564a256a78aa4897f0fdba78b8dd41d88f34e8a9
// keccak256 encoded "SHIP_REMOVER_ROLE": 0x5cbc5b59bdfae9f0aaf4938e5a095024e97b6493a564f95fbb82ddcdbafdc969

// we need to define this, rather than importing from openzeppelin, because their
// version uses 0.8.x and can't use that with Azimuth's 0.4.x
interface IERC721Receiver {
    function onERC721Received(address operator, address from, uint256 tokenId, bytes data)
    public
    returns(bytes4);
}

interface IEcliptic {
    function transferPoint(uint32 _point, address _newOwner, bool _reset) external;
}

interface IAzimuth {
    function getOwner(uint32 _point) external view returns (address);
    function owner() external view returns (address);
}

contract Husk is IERC721Receiver, RBAC, Ownable {
    // IEcliptic public ecliptic;
    // Ecliptic public ecliptic;
    // IAzimuth public azimuth;
    IAzimuth public azimuth;

    string public constant SHIP_ADDER_ROLE = "ship_adder";
    string public constant SHIP_REMOVER_ROLE = "ship_remover";

    event ShipDocked(uint32 indexed tokenID, string hostedUrl);
    event ShipLaunched(uint32 indexed tokenID, address indexed owner, string hostedUrl);

    struct DockedShip {
        uint256 id;
        uint32 tokenID;
        string hostedUrl;
    }

    DockedShip[] private hangar;

    constructor(address _azimuthAddress) {
        azimuth = IAzimuth(_azimuthAddress);

        // since the constructor creates these roles, the contract owner is the 'admin' for these roles.
        // and the contract owner is the DEFAULT_ADMIN_ROLE, which means nobody else can call _setupRole
        /// _setupRole(SHIP_ADDER_ROLE, msg.sender);
        /// _setupRole(SHIP_REMOVER_ROLE, msg.sender);
        addRole(msg.sender, SHIP_ADDER_ROLE);
        addRole(msg.sender, SHIP_REMOVER_ROLE);
        // _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);


        // azimuth = IAzimuth(_azimuthAddress);
    }

    function grantShipAdderRole(address _address) public onlyOwner {
        addRole(_address, SHIP_ADDER_ROLE);
    }
    function grantShipRemoverRole(address _address) public onlyOwner {
        addRole(_address, SHIP_REMOVER_ROLE);
    }

    // This is the callback function that a hosting provider using safeTransferFrom should call
    function onERC721Received(address operator, address from, uint256 tokenID, bytes data)
    public
    returns (bytes4) {
        // require(hasRole(msg.sender, SHIP_ADDER_ROLE), "Must have ship adder role to transfer ship");

        // Ensure that the tokenId can be safely converted to a uint32
        require(tokenID <= 4294967295, "tokenId exceeds maximum for uint32");

        // Convert the tokenID to a uint32
        uint32 tokenID32 = uint32(tokenID);

        // Do something when we receive an NFT
        // pull out the URL from the function call data
        string memory url = string(data);

        // Add it to the hangar (addDockedShip will create the struct)
        addDockedShip(tokenID32, url);

        return this.onERC721Received.selector;
    }
    // to be used something like this:
    // (javascript)
    // let receivingContract = await ReceivingContract.deployed();

    // let data = web3.eth.abi.encodeParameter('string', 'https://sampel-palenet.red-horizon.com');
    // await erc721.safeTransferFrom("0xYourAddress", receivingContract.address, 1, data);


    function addDockedShip(uint32 tokenID, string memory hostedUrl) public {
        // TODO: bring roles back in. currently failing in integration tests for some reason.
        // require(hasRole(msg.sender, SHIP_ADDER_ROLE), "Must have ship adder role to dock ship");

        // TODO: require hostedUrl to be present?
        // TODO: require this to actually be an azimuth point?! we don't want
        // hosting providers accidentally sending us BAYCs

        DockedShip memory newShip = DockedShip({
            id: hangar.length, // the 'id' is based on the current queue of ships
            tokenID: tokenID,
            hostedUrl: hostedUrl
        });

        hangar.push(newShip);
        emit ShipDocked(tokenID, hostedUrl);
    }

    // This function REQUIRES that the hosting provider FIRST make us the 
    // transferProxy for this point. THEN call this function.
    function addDockedShipWithTransfer(uint32 _point, string memory hostedUrl) public {
        // TODO: bring roles back in. currently failing in integration tests for some reason.
        // require(hasRole(msg.sender, SHIP_ADDER_ROLE), "Must have ship adder role to dock ship");

        // TODO: require hostedUrl to be present?

        // Ensure caller owns the point
        require(azimuth.getOwner(_point) == msg.sender, "Sender must own the point");

        // TODO: this won't work, because msg.sender to _ecliptic_ is Husk, not the owner.
        // Transfer to Husk
        IEcliptic ecliptic = IEcliptic(azimuth.owner());
        ecliptic.transferPoint(_point, address(this), false);

        addDockedShip(_point, hostedUrl);
    }


    // Function to get the details of a docked ship
    function getDockedShip(uint256 id) public view returns (uint32, string memory) {
        require(id < hangar.length, "Invalid ship ID");

        DockedShip memory dockedShip = hangar[id];
        return (dockedShip.tokenID, dockedShip.hostedUrl);
    }

    // Function to get the total number of docked ships
    function getDockedShipsCount() public view returns (uint256) {
        return hangar.length;
    }

    // Function to remove a docked ship (LIFO)
    // TODO: should this called as a callback after _this contract_ (or %make) transfers ownership to some address?
    // TODO: implement the azimuth safeTransferFrom required to send a ship to a new user if using callbacks...?
    function transferPointToShipless(address targetAddress) public onlyRole(SHIP_REMOVER_ROLE) returns (uint32, string memory) {
        require(hasRole(msg.sender, SHIP_REMOVER_ROLE), "Must have ship remover role to remove ship");
        require(hangar.length > 0, "No more docked ships");
        // TODO: emit this as an event

        // Step 1: Peek at the next ship on the stack
        DockedShip memory nextShip = hangar[hangar.length - 1];
        uint32 nextShipTokenID = uint32(nextShip.tokenID);

        // Step 2: Try to transfer ownership of the point
        IEcliptic ecliptic = IEcliptic(azimuth.owner());
        ecliptic.transferPoint(nextShipTokenID, targetAddress, false);

        // Step 3: If the transfer didn't throw, remove the ship from the stack
        delete hangar[hangar.length - 1];
        hangar.length--;

        emit ShipLaunched(nextShipTokenID, targetAddress, nextShip.hostedUrl);
        return (nextShip.tokenID, nextShip.hostedUrl);
    }

    // TODO: deprecated? fix this.
    // function destroy() public onlyOwner {
    //     address _owner = owner();
    //     selfdestruct(_owner);
    // }
    // function destroy() public onlyOwner {
    //     address _owner = owner();
    //     address payable payableOwner = payable(address(uint160(_owner)));
    //     selfdestruct(payableOwner);
    // }

    //// AccessControl provides these:
    // Grants `role` to `account`
    // function grantRole(bytes32 role, address account) public virtual;
    // used like: grantRole(SHIP_ADDER_ROLE, some_address);

    // // Revokes `role` from `account`
    // function revokeRole(bytes32 role, address account) public virtual;
    // used like: revokeRole(SHIP_ADDER_ROLE, some_address);

    // // Renounce an assigned `role` from self 
    // function renounceRole(bytes32 role, address account) public virtual;
    // used like:  renounceRole(SHIP_ADDER_ROLE, msg.sender);

    // // Check if an `account` has been granted a `role`
    // function hasRole(bytes32 role, address account) public view returns (bool);
    // used like: if (hasRole(SHIP_ADDER_ROLE, some_address)) {
    //   ... do something ...
    // }
}


// File: openzeppelin-solidity/contracts/ownership/Ownable.sol



pragma solidity ^0.4.24;


/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
  address public owner;


  event OwnershipRenounced(address indexed previousOwner);
  event OwnershipTransferred(
    address indexed previousOwner,
    address indexed newOwner
  );


  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  constructor() public {
    owner = msg.sender;
  }

  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }

  /**
   * @dev Allows the current owner to relinquish control of the contract.
   * @notice Renouncing to ownership will leave the contract without an owner.
   * It will not be possible to call the functions with the `onlyOwner`
   * modifier anymore.
   */
  function renounceOwnership() public onlyOwner {
    emit OwnershipRenounced(owner);
    owner = address(0);
  }

  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param _newOwner The address to transfer ownership to.
   */
  function transferOwnership(address _newOwner) public onlyOwner {
    _transferOwnership(_newOwner);
  }

  /**
   * @dev Transfers control of the contract to a newOwner.
   * @param _newOwner The address to transfer ownership to.
   */
  function _transferOwnership(address _newOwner) internal {
    require(_newOwner != address(0));
    emit OwnershipTransferred(owner, _newOwner);
    owner = _newOwner;
  }
}

// File: openzeppelin-solidity/contracts/access/rbac/Roles.sol



pragma solidity ^0.4.24;


/**
 * @title Roles
 * @author Francisco Giordano (@frangio)
 * @dev Library for managing addresses assigned to a Role.
 * See RBAC.sol for example usage.
 */
library Roles {
  struct Role {
    mapping (address => bool) bearer;
  }

  /**
   * @dev give an address access to this role
   */
  function add(Role storage _role, address _addr)
    internal
  {
    _role.bearer[_addr] = true;
  }

  /**
   * @dev remove an address' access to this role
   */
  function remove(Role storage _role, address _addr)
    internal
  {
    _role.bearer[_addr] = false;
  }

  /**
   * @dev check if an address has this role
   * // reverts
   */
  function check(Role storage _role, address _addr)
    internal
    view
  {
    require(has(_role, _addr));
  }

  /**
   * @dev check if an address has this role
   * @return bool
   */
  function has(Role storage _role, address _addr)
    internal
    view
    returns (bool)
  {
    return _role.bearer[_addr];
  }
}

// File: openzeppelin-solidity/contracts/access/rbac/RBAC.sol



pragma solidity ^0.4.24;

import "./Roles.sol";


/**
 * @title RBAC (Role-Based Access Control)
 * @author Matt Condon (@Shrugs)
 * @dev Stores and provides setters and getters for roles and addresses.
 * Supports unlimited numbers of roles and addresses.
 * See //contracts/mocks/RBACMock.sol for an example of usage.
 * This RBAC method uses strings to key roles. It may be beneficial
 * for you to write your own implementation of this interface using Enums or similar.
 */
contract RBAC {
  using Roles for Roles.Role;

  mapping (string => Roles.Role) private roles;

  event RoleAdded(address indexed operator, string role);
  event RoleRemoved(address indexed operator, string role);

  /**
   * @dev reverts if addr does not have role
   * @param _operator address
   * @param _role the name of the role
   * // reverts
   */
  function checkRole(address _operator, string _role)
    public
    view
  {
    roles[_role].check(_operator);
  }

  /**
   * @dev determine if addr has role
   * @param _operator address
   * @param _role the name of the role
   * @return bool
   */
  function hasRole(address _operator, string _role)
    public
    view
    returns (bool)
  {
    return roles[_role].has(_operator);
  }

  /**
   * @dev add a role to an address
   * @param _operator address
   * @param _role the name of the role
   */
  function addRole(address _operator, string _role)
    internal
  {
    roles[_role].add(_operator);
    emit RoleAdded(_operator, _role);
  }

  /**
   * @dev remove a role from an address
   * @param _operator address
   * @param _role the name of the role
   */
  function removeRole(address _operator, string _role)
    internal
  {
    roles[_role].remove(_operator);
    emit RoleRemoved(_operator, _role);
  }

  /**
   * @dev modifier to scope access to a single role (uses msg.sender as addr)
   * @param _role the name of the role
   * // reverts
   */
  modifier onlyRole(string _role)
  {
    checkRole(msg.sender, _role);
    _;
  }

  /**
   * @dev modifier to scope access to a set of roles (uses msg.sender as addr)
   * @param _roles the names of the roles to scope access to
   * // reverts
   *
   * @TODO - when solidity supports dynamic arrays as arguments to modifiers, provide this
   *  see: https://github.com/ethereum/solidity/issues/2467
   */
  // modifier onlyRoles(string[] _roles) {
  //     bool hasAnyRole = false;
  //     for (uint8 i = 0; i < _roles.length; i++) {
  //         if (hasRole(msg.sender, _roles[i])) {
  //             hasAnyRole = true;
  //             break;
  //         }
  //     }

  //     require(hasAnyRole);

  //     _;
  // }
}