# -*- coding: utf-8 -*-

# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code

from ccxt.async_support.base.exchange import Exchange
from ccxt.abstract.idax import ImplicitAPI
import asyncio
import hashlib
from ccxt.base.types import Any, Balances, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade
from typing import List
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import BadRequest
from ccxt.base.errors import BadSymbol
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import OrderNotFound
from ccxt.base.errors import OrderNotFillable
from ccxt.base.errors import OperationFailed
from ccxt.base.errors import RateLimitExceeded
from ccxt.base.errors import InvalidNonce
from ccxt.base.errors import RequestTimeout
from ccxt.base.decimal_to_precision import DECIMAL_PLACES


class idax(Exchange, ImplicitAPI):

    def describe(self):
        return self.deep_extend(super(idax, self).describe(), {
            'id': 'idax',
            'name': 'Idax',
            'countries': ['MN'],  # Mongolia
            # 12000 per minute => 200 per second
            # 1000ms / 200 = 5
            'rateLimit': 5,
            'certified': False,
            'pro': True,
            'timeDifference': 0,
            'has': {
                'CORS': None,
                'spot': True,
                'margin': None,  # soon
                'swap': False,
                'future': None,  # soon
                'option': False,
                'cancelOrder': True,
                'createOrder': True,
                'deposit': True,
                'fetchBalance': True,
                'fetchClosedOrders': False,
                'fetchCurrencies': False,
                'fetchMarkets': True,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenOrders': True,
                'fetchOrder': True,
                'fetchOrderBook': True,
                'fetchOrders': False,
                'fetchStatus': False,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTime': True,
                'fetchTrades': True,
            },
            'requiredCredentials': {
                'apiKey': True,
                'secret': True,
            },
            'timeframes': {
                '1m': '1min',
                '5m': '5min',
                '15m': '15min',
                '30m': '30min',
                '60m': '60min',
                '1h': '1h',
                '4h': '4h',
                '1d': '1day',
                '1w': '1week',
                '1M': '1month',
            },
            'urls': {
                'logo': 'https://www.idax.exchange/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Flogo-light.dd9b78e4.png&w=128&q=75',
                'api': {
                    'sapiPublic': 'https://openapi.idax.exchange/sapi/v1',
                    'sapiPrivate': 'https://openapi.idax.exchange/sapi/v1',
                },
                'www': 'https://www.idax.exchange',
                'doc': [
                    'https://exchangedocsv2.gitbook.io/open-api-doc/v/english/spot',
                ],
                'api_management': 'https://www.idax.exchange/profile/api-management',
            },
            'api': {
                'sapiPublic': {
                    'get': {
                        'symbols': 1,
                        'depth': 5,
                        'ticker': 5,
                        'klines': 1,
                        'time': 1,
                        'ping': 1,
                        'trades': 5,
                    },
                },
                'sapiPrivate': {
                    'get': {
                        'order': 1,
                        'myTrades': 1,
                        'openOrders': 1,
                        'account': 1,
                    },
                    'post': {
                        'order': 5,
                        'order/test': 1,
                        'batchOrders': 10,
                        'cancel': 5,
                        'batchCancel': 10,
                    },
                },
            },
            'fees': {},
            'precisionMode': DECIMAL_PLACES,
            'options': {
                'sandboxMode': None,
                'defaultType': 'spot',
                'fetchMarkets': [
                    'spot',
                ],
                'adjustForTimeDifference': True,
            },
            'exceptions': {
                'exact': {
                    # error codes
                    #
                    #        1xxx
                    #
                    '-1000': OperationFailed,
                    '-1001': OperationFailed,
                    '-1002': AuthenticationError,
                    '-1003': RateLimitExceeded,
                    '-1004': AuthenticationError,
                    '-1006': OperationFailed,
                    '-1007': RequestTimeout,
                    '-1014': InvalidOrder,
                    '-1015': RateLimitExceeded,
                    '-1016': BadRequest,
                    '-1017': BadRequest,
                    '-1020': BadRequest,
                    '-1021': InvalidNonce,
                    '-1022': AuthenticationError,
                    '-1023': AuthenticationError,
                    '-1024': AuthenticationError,
                    '-1100': BadRequest,
                    '-1101': BadRequest,
                    '-1102': BadRequest,
                    '-1103': BadRequest,
                    '-1104': BadRequest,
                    '-1105': BadRequest,
                    '-1106': BadRequest,
                    '-1111': BadRequest,
                    '-1112': OperationFailed,
                    '-1116': InvalidOrder,
                    '-1117': InvalidOrder,
                    '-1118': InvalidOrder,
                    '-1121': BadSymbol,
                    '-1136': InvalidOrder,
                    '-1138': InvalidOrder,
                    '-1139': InvalidOrder,
                    '-1145': BadRequest,
                    # 2xxx
                    '-2013': OrderNotFound,
                    '-2015': AuthenticationError,
                    '-2016': OrderNotFillable,
                    '-2017': InsufficientFunds,
                },
            },
        })

    async def fetch_markets(self, params={}) -> List[Market]:
        promises = [
            # Will add fetchFuturesMarkets(params) later
            self.fetch_spot_markets(params),
        ]
        awaitedPromises = await asyncio.gather(*promises)
        result = []
        for i in range(0, len(awaitedPromises)):
            result = self.array_concat(result, awaitedPromises[i])
        if self.options['adjustForTimeDifference']:
            await self.load_time_difference()
        return result

    async def fetch_spot_markets(self, params={}) -> List[Market]:
        response = await self.sapiPublicGetSymbols()
        #
        # spot
        #
        #     {
        #         "symbols":[
        #             {
        #                 "symbol":"shib1321usdt",
        #                 "baseAsset":"SHIB1321",
        #                 "baseAssetName":"SHIB",
        #                 "quoteAsset":"USDT",
        #                 "quoteAssetName":"USDT",
        #                 "quantityPrecision":1,
        #                 "pricePrecision":8,
        #             }
        #         ]
        #     }
        symbols = self.safe_list(response, 'symbols', [])
        result: List[Market] = []
        for i in range(0, len(symbols)):
            symbolData = self.safe_dict(symbols, i)
            id = self.safe_string(symbolData, 'symbol').upper()
            baseId = self.safe_string(symbolData, 'baseAsset')
            quoteId = self.safe_string(symbolData, 'quoteAsset')
            base = self.safe_currency_code(self.safe_string(symbolData, 'baseAssetName'))
            if base is None:
                base = self.safe_currency_code(self.safe_string(symbolData, 'baseAsset'))
            quote = self.safe_currency_code(self.safe_string(symbolData, 'quoteAssetName'))
            symbol = base + '/' + quote
            amountPrecision = self.safe_number(symbolData, 'quantityPrecision')
            pricePrecision = self.safe_number(symbolData, 'pricePrecision')
            result.append({
                'id': id,
                'baseId': baseId,
                'quoteId': quoteId,
                'base': base,
                'quote': quote,
                'spot': True,
                'future': False,
                'swap': False,
                'option': False,
                'contract': False,
                'linear': None,
                'margin': False,
                'type': 'spot',
                'maker': 0.01,
                'taker': 0.01,
                'settle': None,
                'settleId': None,
                'tierBased': False,
                'inverse': None,
                'contractSize': None,
                'expiry': None,
                'expiryDatetime': None,
                'strike': None,
                'optionType': None,
                'symbol': symbol,
                'created': None,
                'active': True,
                'info': symbolData,
                'limits': {
                    'leverage': {
                        'min': None,
                        'max': None,
                    },
                    'amount': {
                        'min': None,
                        'max': None,
                    },
                    'price': {
                        'min': None,
                        'max': None,
                    },
                    'cost': {
                        'min': None,
                        'max': None,
                    },
                },
                'precision': {
                    'amount': amountPrecision,
                    'price': pricePrecision,
                },
            })
        return result

    async def fetch_ticker(self, symbol: str, params={}) -> Ticker:
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        request = {
            'symbol': marketId,
        }
        response = await self.sapiPublicGetTicker(self.deep_extend(request, params))
        return self.parse_ticker(response, market)

    async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
        await self.load_markets()
        result = {}
        symbols = self.market_symbols(symbols, None, True, True, True)
        if symbols is None:
            return {}
        length = len(symbols)
        for i in range(0, length):
            symbol = symbols[i]
            market = self.market(symbol)
            marketId = market['id']
            request = {
                'symbol': marketId,
            }
            response = await self.sapiPublicGetTicker(self.deep_extend(request, params))
            ticker = self.parse_ticker(response, market)
            result[marketId] = ticker
        return result

    def parse_ticker(self, response, market: Market) -> Ticker:
        symbol = market['symbol']
        high = self.safe_number(response, 'high')
        low = self.safe_number(response, 'low')
        bid = self.safe_number(response, 'buy')
        ask = self.safe_number(response, 'sell')
        baseVolume = self.safe_number(response, 'vol')
        timestamp = self.safe_number(response, 'time')
        last = self.safe_number(response, 'last')
        open = self.safe_number(response, 'open')
        change = self.safe_number(response, 'rose')
        percentage = (change / open) * 100
        average = None
        if last is not None:
            average = (last + open) / 2
        return self.safe_ticker({
            'symbol': symbol,
            'high': high,
            'low': low,
            'previousClose': None,
            'bid': bid,
            'ask': ask,
            'baseVolume': baseVolume,
            'quoteVolume': None,
            'timestamp': timestamp,
            'last': last,
            'close': last,
            'open': open,
            'vwap': None,
            'change': change,
            'percentage': percentage,
            'datetime': self.iso8601(timestamp),
            'bidVolume': None,
            'askVolume': None,
            'average': average,
            'info': response,
        }, market)

    async def fetch_time(self, params={}) -> float:
        response = await self.sapiPublicGetTime(params)
        return self.safe_number(response, 'serverTime')

    async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        request = {
            'symbol': marketId,
        }
        if limit is not None:
            request['limit'] = limit
        time = await self.fetch_time()
        response = await self.sapiPublicGetDepth(self.deep_extend(request, params))
        return self.parse_order_book(response, symbol, time)

    async def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        interval = self.safe_string(self.timeframes, timeframe)
        request = {
            'symbol': marketId,
            'interval': interval,
        }
        if limit is not None:
            request['limit'] = limit
        if since is not None:
            request['startTime'] = since
        response = await self.sapiPublicGetKlines(self.deep_extend(request, params))
        return self.parse_ohlcvs(response, market, timeframe, since, limit)

    def parse_ohlcv(self, ohlcv: Any, market: Market) -> list:
        timestamp = self.safe_number(ohlcv, 'idx')
        open = self.safe_number(ohlcv, 'open')
        high = self.safe_number(ohlcv, 'high')
        low = self.safe_number(ohlcv, 'low')
        close = self.safe_number(ohlcv, 'close')
        vol = self.safe_number(ohlcv, 'vol')
        return [timestamp, open, high, low, close, vol]

    async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        request = {
            'symbol': marketId,
        }
        if limit is not None:
            request['limit'] = limit
        response = await self.sapiPublicGetTrades(self.deep_extend(request, params))
        trades = self.safe_list(response, 'list')
        return self.parse_trades(trades, market)

    def parse_trade(self, trade: object, market: Market = None) -> Trade:
        amount = self.safe_number(trade, 'qty')
        timestamp = self.safe_number(trade, 'time')
        datetime = self.iso8601(timestamp)
        marketId = self.safe_string(trade, 'symbol')
        symbol = self.safe_symbol(marketId, market)
        price = self.safe_number(trade, 'price')
        side = self.safe_string(trade, 'side').lower()
        isMaker = self.safe_bool(trade, 'isMaker')
        takerOrMaker = None
        order = None
        fee = None
        if isMaker is not None:
            takerOrMaker = 'maker' if (isMaker) else 'taker'
            # Coming from myTrades
            isBuyer = self.safe_bool(trade, 'isBuyer')
            idKey = 'bidId' if (isBuyer) else 'askId'
            order = self.safe_string(trade, idKey)
            arr = symbol.split('/')
            currency = arr[0] if (isBuyer) else arr[1]
            fee = {
                'cost': self.safe_string(trade, 'fee'),
                'currency': currency,
            }
        id = self.safe_string(trade, 'id')
        cost = price * amount
        return self.safe_trade({
            'info': trade,
            'amount': amount,
            'datetime': datetime,
            'id': id,
            'order': order,
            'price': price,
            'timestamp': timestamp,
            'type': None,
            'side': side,
            'symbol': symbol,
            'takerOrMaker': takerOrMaker,
            'cost': cost,
            'fee': fee,
        })

    def nonce(self):
        return self.milliseconds() - self.options['timeDifference']

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        headers = {
            'Content-Type': 'application/json',
        }
        bodyString = None
        if 'body' in params:
            body = params['body']
            params = self.omit(params, 'body')
            bodyString = self.json(body)
        paramsString = ''
        if not self.is_empty(params):
            paramsString = '?' + self.urlencode(params)
        url = self.urls['api'][api]
        url += '/' + path
        if paramsString != '':
            url = url + paramsString
        # Will modify it when support for futures and margins are being implemented
        if api == 'sapiPrivate':
            timestamp = self.number_to_string(self.nonce())
            hashData = timestamp + method + '/sapi/v1/' + path + paramsString
            if bodyString is not None:
                hashData = hashData + bodyString
            signature = self.hmac(self.encode(hashData), self.encode(self.secret), hashlib.sha256)
            headers['X-CH-APIKEY'] = self.apiKey
            headers['X-CH-SIGN'] = signature
            headers['X-CH-TS'] = timestamp
        obj = {
            'url': url,
            'method': method,
            'body': bodyString,
            'headers': headers,
        }
        return obj

    async def fetch_balance(self, params={}) -> Balances:
        await self.load_markets()
        response = await self.sapiPrivateGetAccount()
        return self.parse_balance(response)

    def parse_balance(self, response: Any) -> Balances:
        list = self.safe_list(response, 'balances')
        balances = {
            'info': response,
        }
        for i in range(0, len(list)):
            assetInfo = list[i]
            currencyId = self.safe_string(assetInfo, 'asset')
            code = self.safe_currency_code(currencyId)
            free = self.safe_number(assetInfo, 'free')
            locked = self.safe_number(assetInfo, 'locked')
            total = free + locked
            balances[code] = {
                'free': free,
                'used': locked,
                'total': total,
            }
        timestamp = self.milliseconds()
        datetime = self.iso8601(timestamp)
        balances['timestamp'] = timestamp
        balances['datetime'] = datetime
        return balances

    async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        clientOrderId = self.safe_string(params, 'clientOrderId')
        request = {
            'symbol': marketId,
            'volume': amount,
            'side': side.upper(),
            'type': type.upper(),
        }
        if price is not None:
            request['price'] = price
        if clientOrderId is not None:
            request['clientOrderId'] = clientOrderId
        params['body'] = request
        response = await self.sapiPrivatePostOrder(params)
        return self.parse_order(response, market)

    def parse_order(self, order: Any, market: Market) -> Order:
        id = None
        if 'orderIdString' in order:
            id = self.safe_string(order, 'orderIdString')
        else:
            id = self.safe_string(order, 'orderId')
        timestampKey = 'transactTime'
        if not (timestampKey in order):
            timestampKey = 'time'
        timestamp = self.safe_number(order, timestampKey)
        datetime = self.iso8601(timestamp)
        status = self.parse_status(self.safe_string(order, 'status', '0'))
        marketId = self.safe_string(order, 'symbol')
        symbol = self.safe_symbol(marketId, market)
        orderType = self.safe_string(order, 'type')
        side = self.safe_string(order, 'side').lower()
        price = self.safe_number(order, 'price')
        amount = self.safe_number(order, 'origQty')
        filled = self.safe_number(order, 'executedQty')
        remaining = amount - filled
        average = None
        if 'avgPrice' in order:
            average = self.safe_number(order, 'avgPrice')
        clientOrderId = self.safe_string(order, 'clientOrderId')
        return self.safe_order({
            'id': id,
            'clientOrderId': clientOrderId,
            'datetime': datetime,
            'timestamp': timestamp,
            'lastTradeTimestamp': timestamp,
            'lastUpdateTimestamp': timestamp,
            'status': status,
            'symbol': symbol,
            'type': orderType,
            'side': side,
            'price': price,
            'amount': amount,
            'filled': filled,
            'remaining': remaining,
            'cost': None,
            'trades': None,
            'fee': None,
            'reduceOnly': None,
            'postOnly': None,
            'average': average,
            'info': order,
        }, market)

    def parse_status(self, status: str) -> str:
        if status == '0':
            return 'open'
        dict = {
            'NEW': 'open',
            'PART_FILLED': 'open',
            'CANCELED': 'canceled',
            'PENDING_CANCEL': 'canceled',
            'REJECTED': 'rejected',
            'FILLED': 'closed',
        }
        return dict[status]

    async def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order:
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        request = {
            'orderId': id,
            'symbol': marketId,
        }
        params['body'] = request
        response = await self.sapiPrivatePostCancel(params)
        return self.parse_canceled_order(response, market)

    def parse_canceled_order(self, response, market: Market) -> Order:
        id = self.safe_string(response, 'orderIdString')
        marketId = self.safe_string(response, 'symbol')
        symbol = self.safe_symbol(marketId, market)
        status = self.safe_string(response, 'status')
        orderStatus = None
        if status == 'PENDING_CANCEL' or status == 'CANCELED':
            orderStatus = 'canceled'
        else:
            orderStatus = 'rejected'
        return self.safe_order({
            'id': id,
            'symbol': symbol,
            'status': orderStatus,
        }, market)

    async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order:
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        request = {
            'orderId': id,
            'symbol': marketId,
        }
        response = await self.sapiPrivateGetOrder(self.extend(request, params))
        return self.parse_order(response, market)

    async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        request = {
            'symbol': marketId,
        }
        if limit is not None:
            request['limit'] = limit
        response = await self.sapiPrivateGetOpenOrders(self.deep_extend(request, params))
        arr = self.safe_list(response, 'list', [])
        return self.parse_orders(arr, market)

    async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        request = {
            'symbol': marketId,
        }
        if limit is not None:
            request['limit'] = limit
        if since is not None:
            request['fromId'] = since
        response = await self.sapiPrivateGetMyTrades(self.extend(request, params))
        trades = self.safe_list(response, 'list')
        return self.parse_trades(trades, market)
