contract GenesisKingdomToken is Operator, ERC721Full {
    using SafeERC20 for IERC20;

    uint256 public constant SYSTEM_FEE_COEFF = 10000; // 10x percent, 10000 = 100%

    struct Order {
        // Order ID - in uuid format
        uint128 id;
        // List of land IDs will be exchanged in this order
        uint128[] landIds;
        // The order maker (in this app it's always seller)
        address maker;
        // The order taker (in this app it's always buyer)
        address taker;
        // The seller's desired currencies. This is actually ERC20 contract address
        // For Ethereum as currency, will use special address 0x0
        address[] offeredCurrencies;
        // The seller's desired amount for each currency respestively
        uint256[] offeredAmounts;
        // The currency that buyer uses to fulfill the order
        address fulfilledCurrency;
        // The amount that buyer uses to fulfill the order
        uint256 fulfilledAmount;
        // The type of order
        OrderType otype;
        // The selling method
        OrderSellingMethod sellingMethod;
        // The status of order
        OrderStatus status;
    }

    struct SystemFee {
        uint128 id;
        // % genesis fee per transaction
        uint256 genesisFee;
        // % inviter reward per transaction
        uint256 inviterReward;
        // % buyer reward per transaction
        uint256 buyerReward;
    }

    enum OrderType { OrderIndividual, OrderBundle }

    enum OrderSellingMethod { Acreage, Freely }

    enum OrderStatus { OPEN, HOLDING, FULFILLED, CANCELLED }

    // storage land name
    mapping(uint128 => bytes32) private _nameOfLands;
    // storage land bundle id
    mapping(uint128 => uint128) private _bundleOfLands;
    // storage orders
    mapping(uint128 => Order) private _orders;
    // storage land id or land bundle id in order
    mapping(uint128 => uint128) private _goodIdOfOrders;
    // storage the order holder
    mapping(uint128 => address) private _orderHolders;
    // storage the processed txid
    mapping(string => bool) private _processedTxids;
    // storage the system fees
    mapping(uint128 => SystemFee) private _systemFees;
    // storage the order system fee
    mapping(uint128 => uint128) private _orderFees;

    // fire when receive token
    event Received(address operator, address from, uint256 tokenId, bytes data, uint256 gas);
    // fire when a new order is created
    event OrderCreate(address operator, uint128 indexed orderId);
    // fire when seller want to update order price, selling method, fee
    event OrderUpdate(address operator, uint128 indexed orderId);
    // fire when seller cancel order
    event OrderCancelled(address operator, uint128 indexed orderId);
    // fire when buyer want to buy order by ether
    event OrderHolding(address holder, uint128 indexed orderId);
    // fire when the operator compelte order
    event OrderFulfilled(address operator, uint128 indexed orderId);
    // fire when a new system fee is created
    event SystemFeeCreate(address operator, uint128 feeId);
    // fire when the owner want to withdrawal ETH or ERC20
    event Withdrawal(address operator, address recepient, address currency, uint256 amount);
    // fire when the operator want to refund ETH or ERC20 to buyer
    event Refund(address receiver, uint128 orderId, address currency, uint256 amount);

    constructor(string memory _name, string memory _symbol) public ERC721Full(_name, _symbol) {
        _addWhitelist(address(this));
    }

    /**
     * ========================================================================================
     * [GET] Land
     */
    /**
     * @dev returns the land detail
     * @param id the land id
     * @return land detail
     */
    function getLandDetail(uint128 id)
        external
        view
        returns (
            uint128 landId,
            uint128 landBundleId,
            bytes32 lname,
            address owner
        )
    {
        landId = id;
        lname = _nameOfLands[landId];
        landBundleId = _bundleOfLands[landId];
        owner = ownerOf(landId);
    }

    /**
     * @dev check the token id has already exists
     * @param id the token id
     * @return bool whether the call correctly returned the expected magic value
     */
    function hasExistentToken(uint128 id) external view returns (bool) {
        return _exists(id);
    }

    /**
     * ========================================================================================
     * [SET] Land
     */
    /**
     * @dev the internal method to issue new land
     * @param _owner the owner of the land
     * @param _id the land id
     * @param _name the land name
     * @param _bundleId the land bundle id
     */
    function _issueNewLand(
        address _owner,
        uint128 _id,
        bytes32 _name,
        uint128 _bundleId
    ) internal {
        require(_bundleId != 0, "GK: invalid bundleId");
        require(_name.length != 0, "GK: invalid landName");

        _nameOfLands[_id] = _name;
        _bundleOfLands[_id] = _bundleId;

        _mint(_owner, _id);
    }

    /**
     * @dev issue all lands of the specific bundle
     * @param _owner the owner of the land
     * @param _ids the array of land id
     * @param _names the array of land name
     * @param _bundleId the land bundle id
     */
    function issueNewLandBundle(
        address _owner,
        uint128[] calldata _ids,
        bytes32[] calldata _names,
        uint128 _bundleId
    ) external onlyOwner whenNotPaused {
        require(_ids.length == _names.length, "GK: invalid array length");
        for (uint256 i = 0; i != _ids.length; i++) {
            _issueNewLand(_owner, _ids[i], _names[i], _bundleId);
        }
    }

    /**
     * ========================================================================================
     * [GET] Order
     */
    /**
     * @dev returns the order detail
     * @param _orderId the order id
     * @return the land detail
     */
    function getOrderDetails(uint128 _orderId)
        external
        view
        returns (
            uint128 id,
            uint128[] memory landIds,
            address maker,
            address taker,
            address[] memory offeredCurrencies,
            uint256[] memory offeredAmounts,
            address fulfilledCurrency,
            uint256 fulfilledAmount,
            OrderType otype,
            OrderSellingMethod sellingMethod,
            OrderStatus status
        )
    {
        Order memory order = _orders[_orderId];
        id = order.id;
        landIds = order.landIds;
        maker = order.maker;
        taker = order.taker;
        offeredCurrencies = order.offeredCurrencies;
        offeredAmounts = order.offeredAmounts;
        fulfilledCurrency = order.fulfilledCurrency;
        fulfilledAmount = order.fulfilledAmount;
        otype = order.otype;
        sellingMethod = order.sellingMethod;
        status = order.status;
    }

    /**
     * @dev returns the land id or bundle id in the order
     * @param _orderId the order id
     * @return can be the land id or bundle id
     */
    function getOrderGoodId(uint128 _orderId) external view returns (uint128) {
        return _goodIdOfOrders[_orderId];
    }

    /**
     * @dev returns the system fee in the order
     * @param _orderId the order id
     * @return the system fee id
     */
    function getOrderGenesisFeeId(uint128 _orderId) external view returns (uint128 feeId) {
        return _orderFees[_orderId];
    }

    /**
     * @dev check order id has already exists
     * @param _orderId the order id
     * @return bool whether the call correctly returned the expected magic value
     */
    function hasExistentOrder(uint128 _orderId) external view returns (bool) {
        return _orders[_orderId].id != 0;
    }

    /**
     * ========================================================================================
     * [SET] Order
     */
    function _preValidateOrder(
        uint128 _orderId,
        uint128 _goodId,
        address[] memory _offeredCurrencies,
        uint256[] memory _offeredAmounts,
        uint128 _feeId
    ) internal view {
        require(_orderId != 0, "GK: invalid orderId");
        require(_orders[_orderId].id == 0, "GK: order has already exists");
        require(_goodId != 0, "GK: invalid goodId");
        require(_offeredCurrencies.length == _offeredAmounts.length, "GK: invalid array length");
        require(_systemFees[_feeId].id != 0, "GK: invalid system fee");
    }

    /**
     * @dev the internal method to create new order
     * @param _orderId the order id
     * @param _goodId the land id or bundle id
     * @param _landIds the array of the land id
     * @param _offeredCurrencies the array of the offered currency
     * @param _offeredAmounts the array of the offered amount
     * @param _type the order type
     * @param _sellingMethod the order selling method
     * @param _feeId the system fee id
     */
    function _createOrder(
        uint128 _orderId,
        uint128 _goodId,
        uint128[] memory _landIds,
        address[] memory _offeredCurrencies,
        uint256[] memory _offeredAmounts,
        OrderType _type,
        OrderSellingMethod _sellingMethod,
        uint128 _feeId
    ) internal {
        _orders[_orderId] = Order(
            _orderId,
            _landIds,
            msg.sender,
            address(0),
            _offeredCurrencies,
            _offeredAmounts,
            address(0),
            0,
            _type,
            _sellingMethod,
            OrderStatus.OPEN
        );
        _orderFees[_orderId] = _feeId;
        _goodIdOfOrders[_orderId] = _goodId;

        emit OrderCreate(msg.sender, _orderId);
    }

    /**
     * @dev using for issue and create land bundle order
     * @param _orderId the order id
     * @param _bundleId the land bundle id
     * @param _ids the array of the land id
     * @param _names the array of the land name
     * @param _offeredCurrencies the array of the offered currency
     * @param _offeredAmounts the array of the offered amount
     * @param _sellingMethod the order selling method
     * @param _feeId the system fee id
     */
    function issueAndCreateLandBundleOrder(
        uint128 _orderId,
        uint128 _bundleId,
        uint128[] calldata _ids,
        bytes32[] calldata _names,
        address[] calldata _offeredCurrencies,
        uint256[] calldata _offeredAmounts,
        OrderSellingMethod _sellingMethod,
        uint128 _feeId
    ) external onlyOwner whenNotPaused {
        for (uint256 i = 0; i != _ids.length; i++) {
            _issueNewLand(address(this), _ids[i], _names[i], _bundleId);
        }

        _preValidateOrder(_orderId, _bundleId, _offeredCurrencies, _offeredAmounts, _feeId);

        _createOrder(_orderId, _bundleId, _ids, _offeredCurrencies, _offeredAmounts, OrderType.OrderBundle, _sellingMethod, _feeId);
    }

    /**
     * @dev using for create a new order to sell single land
     * @param _orderId the order id
     * @param _landId the land id
     * @param _offeredCurrencies the array of the offered currency
     * @param _offeredAmounts the array of the offered amount
     * @param _sellingMethod the order selling method
     * @param _feeId the system fee id
     */
    function createSingleLandOrder(
        uint128 _orderId,
        uint128 _landId,
        address[] calldata _offeredCurrencies,
        uint256[] calldata _offeredAmounts,
        OrderSellingMethod _sellingMethod,
        uint128 _feeId
    ) external whenNotPaused {
        uint128[] memory _landIds = new uint128[](1);
        _landIds[0] = _landId;

        _preValidateOrder(_orderId, _landId, _offeredCurrencies, _offeredAmounts, _feeId);

        transferFrom(msg.sender, address(this), _landId);

        _createOrder(_orderId, _landId, _landIds, _offeredCurrencies, _offeredAmounts, OrderType.OrderIndividual, _sellingMethod, _feeId);
    }

    /**
     * @dev using for create a new order to sell land bundle
     * @param _orderId the order id
     * @param _bundleId the land bundle id
     * @param _offeredCurrencies the array of the offered currency
     * @param _offeredAmounts the array of the offered amount
     * @param _sellingMethod the order selling method
     * @param _feeId the system fee id
     */
    function createLandBundleOrder(
        uint128 _orderId,
        uint128 _bundleId,
        uint128[] calldata _landIds,
        address[] calldata _offeredCurrencies,
        uint256[] calldata _offeredAmounts,
        OrderSellingMethod _sellingMethod,
        uint128 _feeId
    ) external whenNotPaused {
        _preValidateOrder(_orderId, _bundleId, _offeredCurrencies, _offeredAmounts, _feeId);

        _batchSafeTransferFrom(address(0), msg.sender, address(this), _landIds);

        _createOrder(_orderId, _bundleId, _landIds, _offeredCurrencies, _offeredAmounts, OrderType.OrderBundle, _sellingMethod, _feeId);
    }

    /**
     * @dev using for update order price, selling method, fee
     * @param _orderId the order id
     * @param _offeredCurrencies the array of the offered currency
     * @param _offeredAmounts the array of the offered amount
     * @param _sellingMethod the order selling method
     * @param _feeId the system fee id
     */
    function updateOrder(
        uint128 _orderId,
        address[] calldata _offeredCurrencies,
        uint256[] calldata _offeredAmounts,
        OrderSellingMethod _sellingMethod,
        uint128 _feeId
    ) external whenNotPaused {
        Order storage order = _orders[_orderId];
        require(order.id != 0, "GK: caller query nonexistent order");
        require(msg.sender == order.maker, "GK: only maker can update order");
        require(order.status == OrderStatus.OPEN, "GK: order not allow to update");
        require(_systemFees[_feeId].id != 0, "GK: invalid system fee");
        require(_offeredCurrencies.length == _offeredAmounts.length, "GK: invalid array length");

        order.offeredCurrencies = _offeredCurrencies;
        order.offeredAmounts = _offeredAmounts;
        order.sellingMethod = _sellingMethod;
        _orderFees[_orderId] = _feeId;

        emit OrderUpdate(msg.sender, order.id);
    }

    /**
     * @dev using for cancel order
     * @param _orderId the order id
     */
    function cancelOrder(uint128 _orderId) external whenNotPaused {
        Order storage order = _orders[_orderId];
        require(order.id != 0, "GK: caller query nonexistent order");
        require(msg.sender == order.maker, "GK: only maker can cancel order");
        require(order.status == OrderStatus.OPEN, "GK: order not allow to cancel");

        if (order.otype == OrderType.OrderIndividual) {
            IERC721(address(this)).safeTransferFrom(address(this), order.maker, order.landIds[0]);
        } else if (order.otype == OrderType.OrderBundle) {
            _batchSafeTransferFrom(address(this), address(this), order.maker, order.landIds);
        }
        order.status = OrderStatus.CANCELLED;

        emit OrderCancelled(msg.sender, order.id);
    }

    /**
     * @dev buyer can take order by staking ether
     * @param _orderId the order id
     */
    function takeOrderByEther(uint128 _orderId) external payable whenNotPaused {
        Order storage order = _orders[_orderId];
        require(order.id != 0, "GK: caller query nonexistent order");
        require(order.maker != msg.sender, "GK: you are owner");
        if (order.status == OrderStatus.OPEN) {
            uint256 _amount = 0;
            for (uint256 i = 0; i != order.offeredCurrencies.length; i++) {
                if (order.offeredCurrencies[i] == address(0)) {
                    _amount = order.offeredAmounts[i];
                    break;
                }
            }
            require(_amount != 0 && msg.value == _amount, "GK: invalid amount");

            order.status = OrderStatus.HOLDING;
            _orderHolders[_orderId] = msg.sender;

            emit OrderHolding(msg.sender, order.id);
        } else {
            // refund ether to buyer
            msg.sender.transfer(msg.value);
            emit Refund(msg.sender, _orderId, address(0), msg.value);
        }
    }

    /**
     * @dev using for complete the order by the system operator
     * @param _orderId the order id
     * @param _taker the buyer address
     * @param _currency the payable currency
     * @param _amount the payable amount
     * @param _txid the taken txid
     */
    function completeOrder(
        uint128 _orderId,
        address _taker,
        address _currency,
        uint256 _amount,
        string calldata _txid
    ) external whenNotPaused {
        require(isOperator(msg.sender), "GK: caller is not operator");
        require(_taker != address(0), "GK: invalid address");
        if (_orderHolders[_orderId] != address(0)) {
            require(_taker == _orderHolders[_orderId], "GK: invalid taker");
        }
        Order storage order = _orders[_orderId];
        require(order.id != 0, "GK: caller query nonexistent order");
        require(order.maker != _taker, "GK: you are owner");
        require(order.status == OrderStatus.OPEN || order.status == OrderStatus.HOLDING, "GK: cannot complete this order");
        require(!_processedTxids[_txid], "GK: txid has processed");

        uint256 amount = 0;
        for (uint256 i = 0; i != order.offeredCurrencies.length; i++) {
            if (_currency == order.offeredCurrencies[i]) {
                amount = order.offeredAmounts[i];
                break;
            }
        }
        require(amount != 0 && amount == _amount, "GK: invalid amount");

        uint256 fee = 0;
        if (order.maker != owner()) {
            SystemFee memory sFee = _systemFees[_orderFees[_orderId]];
            fee = _amount.mul(sFee.genesisFee).div(SYSTEM_FEE_COEFF);
        }

        // The transfer amount equals the offer amount sub the genesis fee
        if (_currency == address(0)) {
            // Transfer ether to the seller
            order.maker.toPayable().transfer(amount.sub(fee));
        } else {
            // Transfer erc20 token to the seller
            IERC20(_currency).safeTransfer(order.maker, amount.sub(fee));
        }

        // Transfer the land to buyer
        if (order.otype == OrderType.OrderIndividual) {
            IERC721(address(this)).safeTransferFrom(address(this), _taker, order.landIds[0]);
        } else if (order.otype == OrderType.OrderBundle) {
            _batchSafeTransferFrom(address(this), address(this), _taker, order.landIds);
        }

        order.taker = _taker;
        order.fulfilledCurrency = _currency;
        order.fulfilledAmount = amount;
        order.status = OrderStatus.FULFILLED;

        _orderHolders[_orderId] = _taker;
        _processedTxids[_txid] = true;

        emit OrderFulfilled(msg.sender, order.id);
    }

    /**
     * ========================================================================================
     * [GET] System Fee
     */
    /**
     * @dev returns the system fee
     * @param _feeId the system fee id
     * @return the system fee detail
     */
    function getSystemFee(uint128 _feeId)
        external
        view
        returns (
            uint128 feeId,
            uint256 genesisFee,
            uint256 inviterReward,
            uint256 buyerReward
        )
    {
        feeId = _systemFees[_feeId].id;
        genesisFee = _systemFees[_feeId].genesisFee;
        inviterReward = _systemFees[_feeId].inviterReward;
        buyerReward = _systemFees[_feeId].buyerReward;
    }

    /**
     * ========================================================================================
     * [SET] System Fee
     */
    /**
     * @dev using for set a new system fee
     * @param _feeId the system fee id
     * @param _genesisFee the genesis fee percentage
     * @param _inviterReward the inviter reward percentage
     * @param _buyerReward the buyer reward percentage
     */
    function setNewSystemFee(
        uint128 _feeId,
        uint256 _genesisFee,
        uint256 _inviterReward,
        uint256 _buyerReward
    ) external onlyOwner whenNotPaused {
        require(_feeId != 0, "GK: invalid feeId");
        require(_systemFees[_feeId].id == 0, "GK: fee has already exists");

        _systemFees[_feeId] = SystemFee(_feeId, _genesisFee, _inviterReward, _buyerReward);
        emit SystemFeeCreate(msg.sender, _feeId);
    }

    /**
     * ========================================================================================
     * [SET] Withdrawal & Refund
     */
    /**
     * @dev the internal method used to withdrawal ETH or ERC20
     * @param _recepient the receiver address
     * @param _currency the withdrawal currency (can be ETH or ERC20)
     * @param _amount the withdrawl amount
     */
    function _withdrawal(
        address _recepient,
        address _currency,
        uint256 _amount
    ) internal {
        require(_recepient != address(0), "GK: invalid address");
        require(_amount != 0, "GK: invalid amount");
        if (_currency == address(0)) {
            require(address(this).balance >= _amount, "GK: balance not enough");
            _recepient.toPayable().transfer(_amount);
        } else {
            uint256 balance = IERC20(_currency).balanceOf(address(this));
            require(balance >= _amount, "GK: balance not enough");
            IERC20(_currency).safeTransfer(_recepient, _amount);
        }
    }

    /**
     * @dev the owner can use to withdrawal ETH or ERC20
     * @param _recepient the receiver address
     * @param _currency the withdrawal currency (can be ETH or ERC20)
     * @param _amount the withdrawal amount
     */
    function withdrawal(
        address _recepient,
        address _currency,
        uint256 _amount
    ) external onlyOwner {
        _withdrawal(_recepient, _currency, _amount);
        emit Withdrawal(msg.sender, _recepient, _currency, _amount);
    }

    /**
     * @dev the operator can use to refund ETH or ERC20 to buyer
     * @param _orderId the refund order id
     * @param _recepient the receiver address
     * @param _currency the refund currency (can be ETH or ERC20)
     * @param _amount the refund amount
     */
    function refund(
        uint128 _orderId,
        address _recepient,
        address _currency,
        uint256 _amount
    ) external whenNotPaused {
        require(isOperator(msg.sender), "GK: caller is not operator");
        require(_orders[_orderId].id != 0, "GK: caller query nonexistent order");
        _withdrawal(_recepient, _currency, _amount);
        emit Refund(_recepient, _orderId, _currency, _amount);
    }

    /**
     * ========================================================================================
     * [SET] Override ERC721Metadata
     */
    function setTokenURI(uint256 tokenId, string calldata _tokenURI) external onlyOwner {
        super._setTokenURI(tokenId, _tokenURI);
    }

    function setBaseURI(string calldata baseURI) external onlyOwner {
        super._setBaseURI(baseURI);
    }

    /**
     * ========================================================================================
     * [SET] Others
     */
    /**
     * @dev the internal method to transfer multiple tokens
     * @param _token the ERC721 token address
     * @param _from the token holder address
     * @param _recepient the receiver address
     * @param _tokenIds the array of the token
     */
    function _batchSafeTransferFrom(
        address _token,
        address _from,
        address _recepient,
        uint128[] memory _tokenIds
    ) internal {
        for (uint256 i = 0; i != _tokenIds.length; i++) {
            if (_token != address(0)) {
                IERC721(_token).safeTransferFrom(_from, _recepient, _tokenIds[i]);
            } else {
                safeTransferFrom(_from, _recepient, _tokenIds[i]);
            }
        }
    }

    /**
     * ========================================================================================
     * [GET] Callback
     */
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes memory data
    ) public returns (bytes4) {
        emit Received(operator, from, tokenId, data, gasleft());
        return 0x150b7a02;
    }
}
function completeOrder(
    uint128 _orderId,
    address _taker,
    address _currency,
    uint256 _amount,
    string calldata _txid
) external whenNotPaused {
    require(isOperator(msg.sender), "GK: caller is not operator");
    require(_taker != address(0), "GK: invalid address");
    if (_orderHolders[_orderId] != address(0)) {
        require(_taker == _orderHolders[_orderId], "GK: invalid taker");
    }
    Order storage order = _orders[_orderId];
    require(order.id != 0, "GK: caller query nonexistent order");
    require(order.maker != _taker, "GK: you are owner");
    require(order.status == OrderStatus.OPEN || order.status == OrderStatus.HOLDING, "GK: cannot complete this order");
    require(!_processedTxids[_txid], "GK: txid has processed");

    uint256 amount = 0;
    for (uint256 i = 0; i != order.offeredCurrencies.length; i++) {
        if (_currency == order.offeredCurrencies[i]) {
            amount = order.offeredAmounts[i];
            break;
        }
    }
    require(amount != 0 && amount == _amount, "GK: invalid amount");

    uint256 fee = 0;
    if (order.maker != owner()) {
        SystemFee memory sFee = _systemFees[_orderFees[_orderId]];
        fee = _amount.mul(sFee.genesisFee).div(SYSTEM_FEE_COEFF);
    }

    // The transfer amount equals the offer amount sub the genesis fee
    if (_currency == address(0)) {
        // Transfer ether to the seller
        order.maker.toPayable().transfer(amount.sub(fee));
    } else {
        // Transfer erc20 token to the seller
        IERC20(_currency).safeTransfer(order.maker, amount.sub(fee));
    }

    // Transfer the land to buyer
    if (order.otype == OrderType.OrderIndividual) {
        IERC721(address(this)).safeTransferFrom(address(this), _taker, order.landIds[0]);
    } else if (order.otype == OrderType.OrderBundle) {
        _batchSafeTransferFrom(address(this), address(this), _taker, order.landIds);
    }

    order.taker = _taker;
    order.fulfilledCurrency = _currency;
    order.fulfilledAmount = amount;
    order.status = OrderStatus.FULFILLED;

    _orderHolders[_orderId] = _taker;
    _processedTxids[_txid] = true;

    emit OrderFulfilled(msg.sender, order.id);
}

/**
 * @dev using for complete the order by the system operator
 * @param _orderId the order id
 * @param _taker the buyer address
 * @param _currency the payable currency
 * @param _amount the payable amount
 * @param _txid the taken txid
 */