contract AssetsValue {
    // using safe math calculation
    using SafeMath for uint256;

    // for being secure during transactions between users and contract gonna use SafeERC20 lib
    using SafeERC20 for IERC20;

    // The id for ETH asset is 0x0 address
    // the rest assets should have their token contract address
    address internal _ethAssetIdentificator = address(0);

    // Order details which is available by JAVA long number
    struct OrderDetails {
        // does the order has been deposited
        bool created;
        // the 0x0 for Ethereum and ERC contract address for tokens
        address asset;
        // tokens/eth amount
        uint256 amount;
        // the status (deposited/withdrawn)
        bool withdrawn;
        //Creation time
        uint256 initTimestamp;
    }

    // Each user has his own state and details
    struct User {
        // user exist validation bool
        bool exist;
        // contract order index
        uint256 index;
        // contract index (0, 1, 2 ...) => exchange order number (JAVA long number)
        mapping(uint256 => uint256) orderIdByIndex;
        // JAVA long number => order details
        mapping(uint256 => OrderDetails) orders;
    }

    // ETH wallet => Assets => value
    mapping(address => User) private _users;

    modifier orderIdNotExist(
        uint256 orderId,
        address user
    ) {
        require(_users[user].orders[orderId].created == false, "orderIdIsNotDeposited: user already deposit this orderId");
        _;
    }

    // Events
    event AssetDeposited(uint256 orderId, address indexed user, address indexed asset, uint256 amount);
    event AssetWithdrawal(uint256 orderId, address indexed user, address indexed asset, uint256 amount);

    // -----------------------------------------
    // EXTERNAL
    // -----------------------------------------

    function deposit(
        uint256 orderId
    ) public orderIdNotExist(orderId, msg.sender) payable {
        require(msg.value != 0, "deposit: user needs to transfer ETH for calling this method");

        _deposit(orderId, msg.sender, _ethAssetIdentificator, msg.value);
    }

    function deposit(
        uint256 orderId,
        uint256 amount,
        address token
    ) public orderIdNotExist(orderId, msg.sender) {
        require(token != address(0), "deposit: invalid token address");
        require(amount != 0, "deposit: user needs to fill transferable tokens amount for calling this method");

        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        _deposit(orderId, msg.sender, token, amount);
    }

    function withdraw(
        uint256 orderId
    ) external {
        // validation of the user existion
        require(_doesUserExist(msg.sender) == true, "withdraw: the user is not active");

        // storing the order information (asset and amount)
        OrderDetails memory order = _getDepositedOrderDetails(orderId, msg.sender);
        address asset = order.asset;
        uint256 amount = order.amount;

        // order amount validation, it should not be zero
        require(order.withdrawn == false, "withdraw: this order Id has been already withdrawn or waiting for the swap");

        _withdrawOrderBalance(orderId, msg.sender);

        if (asset == _ethAssetIdentificator) {
            msg.sender.transfer(amount);
        } else {
            IERC20(asset).safeTransfer(msg.sender, amount);
        }

        emit AssetWithdrawal(orderId, msg.sender, asset, amount);
    }

    // -----------------------------------------
    // INTERNAL
    // -----------------------------------------

    function _deposit(
        uint256 orderId,
        address sender,
        address asset,
        uint256 amount
    ) internal {
        _activateIfUserIsNew(sender);
        _depositOrderBalance(orderId, sender, asset, amount);

        _users[sender].index += 1;

        emit AssetDeposited(orderId, sender, asset, amount);
    }

    function _doesUserExist(
        address user
    ) internal view returns (bool) {
        return _users[user].exist;
    }

    function _activateIfUserIsNew(
        address user
    ) internal returns (bool) {
        if (_doesUserExist(user) == false) {
            _users[user].exist = true;
        }
        return true;
    }

    function _getDepositedOrderDetails(
        uint256 orderId,
        address user
    ) internal view returns (OrderDetails memory order) {
        return _users[user].orders[orderId];
    }

    function _depositOrderBalance(
        uint256 orderId,
        address user,
        address asset,
        uint256 amount
    ) internal returns (bool) {
        _users[user].orderIdByIndex[_users[user].index] = orderId;
        _users[user].orders[orderId] = OrderDetails(true, asset, amount, false, block.timestamp);
        return true;
    }

    function _withdrawOrderBalance(
        uint256 orderId,
        address user
    ) internal returns (bool) {
        _users[user].orders[orderId].withdrawn = true;
        return true;
    }

    // -----------------------------------------
    // GETTERS
    // -----------------------------------------

    function doesUserExist(
        address user
    ) external view returns (bool) {
        return _doesUserExist(user);
    }

    function getUserFilledDeposits(
        address user
    ) external view returns (
        uint256[] memory orderIds,
        uint256[] memory amounts,
        uint256[] memory initTimestamps
    ) {
        // amount of deposits which user has been done
        uint256 depositsLength = _users[user].index;

        // init empty arrays which should been returned
        orderIds = new uint256[](depositsLength);
        amounts = new uint256[](depositsLength);
        initTimestamps = new uint256[](depositsLength);

        uint256 j = 0;
        for (uint256 i = 0; i <= depositsLength; i++) {
            uint256 orderId = _users[user].orderIdByIndex[i];
            if (_users[user].orders[orderId].created) {
                orderIds[j] = orderId;

                OrderDetails memory order = _users[user].orders[orderId];
                amounts[j] = order.amount;
                initTimestamps[j] = order.initTimestamp;
                j++;
            }
        }

        return (
        orderIds,
        amounts,
        initTimestamps
        );
    }

    function getUserDepositsAmount(
        address user
    ) external view returns (
        uint256
    ) {
        return _users[user].index;
    }

    function getDepositedOrderDetails(
        uint256 orderId,
        address user
    ) external view returns (
        bool created,
        address asset,
        uint256 amount,
        bool withdrawn
    ) {
        OrderDetails memory order = _getDepositedOrderDetails(orderId, user);
        return (
        order.created,
        order.asset,
        order.amount,
        order.withdrawn
        );
    }
}
/**
 * @title AssetsValue
 * @dev The contract which hold all tokens and ETH as a assets
 * Also should be responsible for the balance increasing/decreasing and validation
 */
function _deposit(
    uint256 orderId,
    address sender,
    address asset,
    uint256 amount
) internal {
    _activateIfUserIsNew(sender);
    _depositOrderBalance(orderId, sender, asset, amount);

    _users[sender].index += 1;

    emit AssetDeposited(orderId, sender, asset, amount);
}

// -----------------------------------------
// INTERNAL
// -----------------------------------------