pragma solidity >=0.4.22 <0.6.0;

contract CryptoConstellationAccessControl  {
    
    address public ceoAddress;
    address public ctoAddress;
    address public cfoAddress;
    address public cooAddress;

    bool public paused = false;
    
    mapping (address => string) public userNickName;

    modifier onlyCEO() {
        require(msg.sender == ceoAddress);
        _;
    }
    
    modifier onlyCTO() {
        require(msg.sender == ctoAddress);
        _;
    }

    modifier onlyCFO() {
        require(msg.sender == cfoAddress);
        _;
    }

    modifier onlyCOO() {
        require(msg.sender == cooAddress);
        _;
    }

    modifier onlyCLevel() {
        require(
            msg.sender == cooAddress ||
            msg.sender == ceoAddress ||
            msg.sender == cfoAddress ||
            msg.sender == ctoAddress
        );
        _;
    }
    
    function setCEO(address _newCEO) external onlyCEO {
        require(_newCEO != address(0));
        ceoAddress = _newCEO;
    }
    
    function setCTO(address _newCTO) external onlyCEO {
        require(_newCTO != address(0));
        ctoAddress = _newCTO;
    }

    function setCFO(address _newCFO) external onlyCEO {
        require(_newCFO != address(0));
        cfoAddress = _newCFO;
    }

    function setCOO(address _newCOO) external onlyCEO {
        require(_newCOO != address(0));
        cooAddress = _newCOO;
    }

    function setNickName(address _user, string calldata _nickName) external returns (bool) {
        require(_user != address(0));
        userNickName[_user] = _nickName;
    }

    function getNickName(address _user) external view returns (string memory _nickname) {
        require(_user != address(0));

        _nickname = userNickName[_user];
    }
    
    modifier whenNotPaused() {
        require(!paused);
        _;
    }

    modifier whenPaused {
        require(paused);
        _;
    }

    function pause() external onlyCLevel whenNotPaused {
        paused = true;
    }
    
    function unpause() external onlyCEO whenPaused {
        // can't unpause if contract was upgraded
        paused = false;
    }
}

library SafeMath {
    
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }
        uint256 c = a * b;
        assert(c / a == b);
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        assert(b <= a);
        return a - b;
    }

    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        assert(c >= a);
        return c;
    }
}

contract CryptoConstellationBase is CryptoConstellationAccessControl {

    using SafeMath for uint256;
    
    event ConstellationCreation(address indexed _owner, uint256 indexed _tokenId);
    
    event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

    event Bought (uint256 indexed _tokenId, address indexed _owner, uint256 _price);

    event Sold (uint256 indexed _tokenId, address indexed _owner, uint256 _price);
    
    struct Constellation {
        string name;
        string description;
        string ipfsHash;
        uint64 creationTimestamp;
    }

    
    uint256 internal increaseLimit1 = 0.02 ether;
    uint256 internal increaseLimit2 = 0.5 ether;
    uint256 internal increaseLimit3 = 2.0 ether;
    uint256 internal increaseLimit4 = 5.0 ether;
    
    Constellation[] constellations;
    
    mapping (uint256 => address) public constellationCurrentOwner;
    
    mapping (address => uint256) internal ownershipTokenCount;

    mapping (uint256 => uint256) internal startingPriceOfConstellation;

    mapping (uint256 => uint256) internal priceOfConstellation;

    mapping (uint256 => address) internal approvedOfConstellation;
    
    
    modifier onlyOwner(uint _propId) {
		require(constellationCurrentOwner[_propId] == msg.sender);
		_;
	}

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        
        // Since the number of assets is capped to 2^32 we can't overflow this
        ownershipTokenCount[_to]++;
        // transfer ownership
        constellationCurrentOwner[_tokenId] = _to;

        approvedOfConstellation[_tokenId] = address(0);
        // When creating new kittens _from is 0x0, but we can't account that address.
        if (_from != address(0)) {
            ownershipTokenCount[_from]--;
        }
        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }
    
    
    function calculateNextPrice (uint256 _price) public view returns (uint256 _nextPrice) {
        if (_price < increaseLimit1) {
            return _price.mul(200).div(95);
        } else if (_price < increaseLimit2) {
            return _price.mul(135).div(96);
        } else if (_price < increaseLimit3) {
            return _price.mul(125).div(97);
        } else if (_price < increaseLimit4) {
            return _price.mul(117).div(97);
        } else {
            return _price.mul(115).div(98);
        }
    }

    function calculateDevCut (uint256 _price) public view returns (uint256 _devCut) {
        if (_price < increaseLimit1) {
            return _price.mul(10).div(100); // 10%
        } else if (_price < increaseLimit2) {
            return _price.mul(9).div(100); // 9%
        } else if (_price < increaseLimit3) {
            return _price.mul(8).div(100); // 8%
        } else if (_price < increaseLimit4) {
            return _price.mul(7).div(100); // 7%
        } else {
            return _price.mul(6).div(100); // 6%
        }
    }

    
    function createConstellation(
        string calldata _name,
        string calldata _description,
        string calldata _ipfsHash,
        uint256 _price,
        address _owner
    )
        external
        whenNotPaused
        onlyCLevel
        returns (uint)
    {
        return _createConstellation(_name, _description, _ipfsHash, _price, _owner);
    }
    
    function _createConstellation(
        string memory _name,
        string memory _description,
        string memory _ipfsHash,
        uint256 _price,
        address _owner
    )
        internal
        whenNotPaused
        onlyCLevel
        returns (uint)
    {
        
        Constellation memory _constellation = Constellation({
            name: _name,
            description: _description,
            ipfsHash: _ipfsHash,
            creationTimestamp: uint64(block.timestamp)
        });
        uint256 newConstellationId = constellations.push(_constellation) - 1;

        require(newConstellationId == uint256(uint32(newConstellationId)));

        startingPriceOfConstellation[newConstellationId] = _price;
        priceOfConstellation[newConstellationId] = _price;

        // emit the birth event
        emit ConstellationCreation(_owner, newConstellationId);

        _transfer(address(0), _owner, newConstellationId);

        return newConstellationId;
    }
   
}


contract ERC721  {
    
    // Required methods
    function totalSupply() public view returns (uint256 total);
    function balanceOf(address _owner) public view returns (uint256 balance);
    function ownerOf(uint256 _tokenId) public view returns (address owner);
    function approve(address _to, uint256 _tokenId) external;
    function transfer(address _to, uint256 _tokenId) external;
    function transferFrom(address _from, address _to, uint256 _tokenId) external;

    // Events
    event Transfer(address from, address to, uint256 tokenId);
    event Approval(address owner, address approved, uint256 tokenId);

    // Optional
    // function name() public view returns (string name);
    // function symbol() public view returns (string symbol);
    // function tokensOfOwner(address _owner) external view returns (uint256[] tokenIds);
    // function tokenMetadata(uint256 _tokenId, string _preferredTransport) public view returns (string infoUrl);

    // ERC-165 Compatibility (https://github.com/ethereum/EIPs/issues/165)
    function supportsInterface(bytes4 _interfaceID) external view returns (bool);
    
}

contract ERC721Metadata {
    /// @dev Given a token Id, returns a byte array that is supposed to be converted into string.
    function getMetadata(uint256 _tokenId, string memory) public pure returns (bytes32[4] memory buffer, uint256 count) {
        if (_tokenId == 1) {
            buffer[0] = "Hello World! :D";
            count = 15;
        } else if (_tokenId == 2) {
            buffer[0] = "I would definitely choose a medi";
            buffer[1] = "um length string.";
            count = 49;
        } else if (_tokenId == 3) {
            buffer[0] = "Lorem ipsum dolor sit amet, mi e";
            buffer[1] = "st accumsan dapibus augue lorem,";
            buffer[2] = " tristique vestibulum id, libero";
            buffer[3] = " suscipit varius sapien aliquam.";
            count = 128;
        }
    }
}

contract CryptoConstellationOwnership is CryptoConstellationBase, ERC721 {
    
    string public constant name = "CryptoConstellation";
    string public constant symbol = "CCL";

    ERC721Metadata public erc721Metadata;

    bytes4 constant InterfaceSignature_ERC165 =
        bytes4(keccak256('supportsInterface(bytes4)'));

    bytes4 constant InterfaceSignature_ERC721 =
        bytes4(keccak256('name()')) ^
        bytes4(keccak256('symbol()')) ^
        bytes4(keccak256('totalSupply()')) ^
        bytes4(keccak256('balanceOf(address)')) ^
        bytes4(keccak256('ownerOf(uint256)')) ^
        bytes4(keccak256('approve(address,uint256)')) ^
        bytes4(keccak256('transfer(address,uint256)')) ^
        bytes4(keccak256('transferFrom(address,address,uint256)')) ^
        bytes4(keccak256('tokensOfOwner(address)')) ^
        bytes4(keccak256('tokenMetadata(uint256,string)'));


    function supportsInterface(bytes4 _interfaceID) external view returns (bool)
    {
        
        return ((_interfaceID == InterfaceSignature_ERC165) || (_interfaceID == InterfaceSignature_ERC721));
    }

    function setMetadataAddress(address _contractAddress) public onlyCEO {
        erc721Metadata = ERC721Metadata(_contractAddress);
    }


    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
        return constellationCurrentOwner[_tokenId] == _claimant;
    }
    
    function _approvedFor(uint256 _tokenId) internal view returns (address) {
        return approvedOfConstellation[_tokenId];
    }

    function _approve(uint256 _tokenId, address _to) internal {
        require(msg.sender != _to);
        require(tokenExists(_tokenId));
        require(ownerOf(_tokenId) == msg.sender);

        if (_to == address(0)) {
            if (approvedOfConstellation[_tokenId] != address(0)) {
                delete approvedOfConstellation[_tokenId];
                emit Approval(msg.sender, address(0), _tokenId);
            }
        } else {
            approvedOfConstellation[_tokenId] = _to;
            emit Approval(msg.sender, _to, _tokenId);
        }
    }

    function ownerOf (uint256 _itemId) public view returns (address _owner) {
        return constellationCurrentOwner[_itemId];
    }

    function balanceOf(address _owner) public view returns (uint256 count) {
        return ownershipTokenCount[_owner];
    }

    function buy(uint256 _tokenId) payable external whenNotPaused
    {
        require(priceOf(_tokenId) > 0);
        require(ownerOf(_tokenId) != address(0));
        require(msg.value >= priceOf(_tokenId));
        require(ownerOf(_tokenId) != msg.sender);
        require(msg.sender != address(0));

        address payable oldOwner = address(uint160(ownerOf(_tokenId)));
        address payable newOwner = msg.sender;
        uint256 price = priceOf(_tokenId);
        uint256 excess = msg.value.sub(price);

        _transfer(oldOwner, newOwner, _tokenId);
        priceOfConstellation[_tokenId] = nextPriceOf(_tokenId);

        emit Bought(_tokenId, newOwner, price);
        emit Sold(_tokenId, oldOwner, price);

        // Devevloper's cut which is left in contract and accesed by
        // `withdrawAll` and `withdrawAmountTo` methods.
        uint256 devCut = calculateDevCut(price);

        // Transfer payment to old owner minus the developer's cut.
        oldOwner.transfer(price.sub(devCut));

        if (excess > 0) {
            newOwner.transfer(excess);
        }
    }


    function approve(address _to, uint256 _tokenId) external whenNotPaused
    {
        require(_owns(msg.sender, _tokenId));
        
        _approve(_tokenId, _to);
    }

    function transfer(address _to, uint256 _itemId) external {
        require(msg.sender == ownerOf(_itemId));
        _transfer(msg.sender, _to, _itemId);
    }

    function transferFrom(address _from, address _to, uint256 _tokenId) external whenNotPaused
    {
        require(_to != address(0));
        require(_to != address(this));
        
        require(_approvedFor(_tokenId) == msg.sender);
        require(_owns(_from, _tokenId));


        _transfer(_from, _to, _tokenId);
    }
    
    

    function totalSupply() public view returns (uint) {
        return constellations.length - 1;
    }

    function tokenExists(uint256 _itemId) public view returns (bool _exists) {
    return priceOf(_itemId) > 0;
    }

    function startingPriceOf(uint256 _itemId) public view returns (uint256 _startingPrice) {
        return startingPriceOfConstellation[_itemId];
    }

    function priceOf(uint256 _itemId) public view returns (uint256 _price) {
        return priceOfConstellation[_itemId];
    }

    function nextPriceOf(uint256 _itemId) public view returns (uint256 _nextPrice) {
        return calculateNextPrice(priceOf(_itemId));
    }


    function tokensOfOwner(address _owner) external view returns(uint256[] memory ownerTokens) {
        uint256 tokenCount = balanceOf(_owner);

        if (tokenCount == 0) {
            // Return an empty array
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 totalConstellation = totalSupply();
            uint256 resultIndex = 0;

            uint256 constellationId;

            for (constellationId = 1; constellationId <= totalConstellation; constellationId++) {
                if (constellationCurrentOwner[constellationId] == _owner) {
                    result[resultIndex] = constellationId;
                    resultIndex++;
                }
            }

            return result;
        }
    }


    function _memcpy(uint _dest, uint _src, uint _len) private pure {
        // Copy word-length chunks while possible
        for(; _len >= 32; _len -= 32) {
            assembly {
                mstore(_dest, mload(_src))
            }
            _dest += 32;
            _src += 32;
        }

        // Copy remaining bytes
        uint256 mask = 256 ** (32 - _len) - 1;
        assembly {
            let srcpart := and(mload(_src), not(mask))
            let destpart := and(mload(_dest), mask)
            mstore(_dest, or(destpart, srcpart))
        }
    }
    
    
    function _toString(bytes32[4] memory _rawBytes, uint256 _stringLength) private pure returns (string memory) {
        string memory outputString = new string(_stringLength);
        uint256 outputPtr;
        uint256 bytesPtr;

        assembly {
            outputPtr := add(outputString, 32)
            bytesPtr := _rawBytes
        }

        _memcpy(outputPtr, bytesPtr, _stringLength);

        return outputString;
    }


    function tokenMetadata(uint256 _tokenId, string calldata _preferredTransport) external view returns (string memory infoUrl) {
        bytes32[4] memory buffer;
        uint256 count;
        (buffer, count) = erc721Metadata.getMetadata(_tokenId, _preferredTransport);

        return _toString(buffer, count);
    }

    function withdrawAll () onlyCLevel external {
        address payable companyAddress = address(uint160(ceoAddress));
        companyAddress.transfer(address(this).balance);
    }

    function withdrawAmount (uint256 _amount) onlyCLevel external {
        address payable companyAddress = address(uint160(ceoAddress));
        companyAddress.transfer(_amount);
    }
}

contract CryptoConstellationCore is CryptoConstellationOwnership{
    
    constructor() public{

        // the creator of the contract is the initial CEO
        ceoAddress = msg.sender;

        // the creator of the contract is also the initial CTO
        ctoAddress = msg.sender;

    }
    
    
    function getConstellation(uint256 _id)
        external
        view
        returns (
        string memory _name,
        string memory _description,
        string memory _ipfsHash,
        address _owner, 
        uint256 _startingPrice, 
        uint256 _price, 
        uint256 _nextPrice
    ) {
        Constellation memory constellation = constellations[_id];
        
        _name = constellation.name;
        _description = constellation.description;
        _ipfsHash = constellation.ipfsHash;
        _owner = ownerOf(_id);
        _startingPrice = startingPriceOf(_id);
        _price = priceOf(_id);
        _nextPrice = nextPriceOf(_id);
        
    }
    
}