"use strict"

class GeminiSandbox extends Exchange {
	static meta = {
		aliases: [
			"GEMINISANDBOX",
			"GEMINI-SANDBOX"
		],
		endpoint: "https://api.sandbox.gemini.com",
		markets: "/cache/gemini/market_data_sandbox.json",
		fee: 0.01,
		fields: {
			public: {
				label: "API Key",
			},
			private: {
				label: "API Secret",
			},
		},
		name: "Gemini Sandbox",
		permissions: {
			origins: [
				"https://api.sandbox.gemini.com/*"
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		version: "v1",
		website: "https://exchange.sandbox.gemini.com",
		desc: "The next generation digital asset platform built for businesses and investors. Buy, sell, and store digital assets with superior trading features, security, and regulatory oversight.",
		testSymbol: "BTCUSD", noOrigin: true
	}
	constructor(meta = GeminiSandbox.meta) {
		super(meta)
	}

	async req(method, resource, params = {}) {
		resource = "/" + this.meta.version + resource

		const headers = {"Cache-Control": "no-cache", "Content-Type": "text/plain"}
		if (method !== 'GET') {
			const credentials = this.getCredentials("private")
			params.request = resource
			params.nonce = this.getNonce()
			params = JSON.stringify(params)
			const payload = btoa(params)
			headers["X-GEMINI-APIKEY"] = credentials.public
			headers["X-GEMINI-PAYLOAD"] = payload
			headers["X-GEMINI-SIGNATURE"] = CryptoJS.HmacSHA384(payload, credentials.private).toString(CryptoJS.enc.Hex)
		} else this.hasAccess()

		return await request.call(this, method, this.meta.endpoint + resource, params, headers)
	}

	async account(currency) {
		const data = await this.req('POST', "/balances")
		let balances = {}

		data.forEach(item => {
			let amount = Number(item.amount)

			if (item.type === "exchange" && (amount || item.currency === currency)) {
				balances[item.currency] = {
					available: Number(item.available),
					balance: amount
				}
			}
		})
		return balances
	}

	async symbolInfoImpl(cache) {
		return await request.call(this, 'GET', rt.getURL(this.meta.markets), null, null)
	}

	async symbolTickerImpl(symbol) {
		const ticker = await this.req('GET', "/pubticker/"+symbol.toLowerCase(), null, null, "json")
		return ticker && {
			ask: Number(ticker.ask),
			bid: Number(ticker.bid),
			mid: (Number(ticker.ask) + Number(ticker.bid)) / 2,
			last: Number(ticker.last)
		}
	}

	async trade(cmd, ohlc = {}) {
		if (cmd.isMargin) {
			throw new Error(`${this.getName()} does not support Margin trading!`)
		}

		const market = await this.symbolInfo(cmd.s)
		const ticker = (cmd.pc && !cmd.up && this.getPrice(cmd.s)) || (await this.symbolTicker(cmd.up, cmd.s))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${cmd.s} is not available!`)
		}
		this.checkSlip(cmd, ohlc, cmd.s, ticker)
		if (cmd.up) {
			this.updatePrice(cmd.s, ticker)
			if (!cmd.b && !cmd.ub) {
				return ticker
			}
		}

		const currency = cmd._currency = cmd.isBid ? market.quoteCurrency : market.baseCurrency
		let balance = {available: 0, balance: 0}
		if (cmd.q.wasPercent() || cmd.ub) {
			balance = cmd.bc && !cmd.ub && this.getBalance(currency)
			const balances = !balance && (await this.account(currency))
			balance = balance || balances[currency]
			this.checkBalance(cmd, balance, currency)
			if (!cmd.bc || cmd.ub) {
				this.updateBalance(balances, currency)
				if (cmd.ub) {
					return Object.assign(ticker, balance)
				}
			}
		}

		const first = ticker[cmd.pr] || this.ohlc(cmd, ohlc, 'pr') || ((cmd.isBid && cmd.t !== "market") || (cmd.isAsk && cmd.t === "market") ? ticker.bid : ticker.ask)
		let price = cmd.fp ? cmd.fp.resolve(market.pricePrecision) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(market.pricePrecision)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated)")
		}

		let available = cmd.y === "equity" ? balance.balance : balance.available
		available = (1 - this.meta.fee) * available
		if (cmd.isBid) {
			available /= price
		}

		let order = {
			symbol: cmd.s,
			amount: cmd.q._().div(cmd.u === "currency" && !cmd.q.wasPercent() ? price : 1)
				.reference(available).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc)).minmax(cmd.minq, cmd.maxq).resolve(market.quantityPrecision),
			price: cmd.t === "market" ? new NumberObject('1%').mul(cmd.isBid ? 1 : -1).relative(cmd.isBid ? ticker.ask : ticker.bid).resolve(market.pricePrecision) : price,
			side: cmd.isBid ? "buy" : "sell",
			options: ({fok: ["fill-or-kill"], ioc: ["immediate-or-cancel"], market: ["immediate-or-cancel"], post: ["maker-or-cancel"]})[cmd.t] || [],
			type: "exchange limit"
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.amount = ohlc.left.toFixed(market.quantityPrecision)
		}
		if (market.minimumTradeSize && order.amount < market.minimumTradeSize) {
			this.warn(`order quantity below instrument minimum of ${market.minimumTradeSize} – use minq= to set minimum!`)
		} else if (order.amount <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated)")
		}
		if (cmd.id) {
			order.client_order_id = this.uniqueID(cmd.id)
		}
		if (cmd.tp || cmd.sl || cmd.so) {
			const param = cmd.tp && 'tpref' || cmd.sl && 'slref' || cmd.so && 'soref'
			order.type = "exchange stop limit"
			order.stop_price = (cmd.tp || cmd.sl || cmd.so)._().relative(this.ohlc(cmd, ohlc, param, ticker, price) || first).resolve(market.pricePrecision)
		}

		this.info("placing order: "+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		return await this.req('POST', "/order/new", order)
	}

	async ordersCancel(cmd, ohlc = {}) {
		let orders = await this.req('POST', "/orders")
		if (!orders || !orders.filter) {
			this.msgOrderErr()
			return false
		}
		orders = orders.filter(order => order.symbol.toUpperCase() === cmd.s)
		if (!orders.length) {
			this.msgOrdersNone()
			return false
		}

		const ids = (cmd.id || "").split(',')
		const match = cmd.cr ? false : true
		const tickers = {}
		await this.filterOrderStart(cmd, ohlc, tickers, orders)
		const totalOrders = orders.length
		orders = orders.filter(order => {
			if ((cmd.isBid && order.side !== "buy") || (cmd.isAsk && order.side !== "sell")) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (cmd.id && (!order.client_order_id || !order.client_order_id.startsWithAny(ids, true))) {
				return !match
			}
			if (!this.filterOrder(cmd, ohlc, tickers, order)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(orders.length).getMax() < orders.length) {
			const sort = {
				"newest": 	["timestamp", true],
				"oldest": 	["timestamp", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["remaining_amount", true],
				"smallest": ["remaining_amount", false]
			}
			cmd.cmo === 'random' ? shuffle(orders) : sortByIndex(orders, sort[cmd.cmo][0], sort[cmd.cmo][1])
			orders = orders.slice(0, cmd.cm.resolve(0))
		}

		this.msgOrders(cmd, orders, totalOrders)
		if (cmd.ch || cmd.d) {
			orders.length && this.msgDisabled(cmd)
			return orders
		}

		let cancelOrders = []
		for (const order of orders) {
			const result = await this.req('POST', "/order/cancel", {order_id: order.order_id})
			cancelOrders.push(result)
		}
		return cancelOrders
	}

	async positionsCloseAll(cmd) {
		throw new ReferenceError(this.getName() + " does not support Margin trading!")
	}
}

class Gemini extends GeminiSandbox {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"GEMINI"
		],
		endpoint: "https://api.gemini.com",
		markets: "/cache/gemini/market_data.json",
		name: "Gemini",
		permissions: {
			origins: [
				"https://api.gemini.com/*"
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"gemin1",
				"gemin2",
			],
		},
		website: "https://www.gemini.com",
	})
	constructor(meta = Gemini.meta) {
		super(meta)
	}
}

Broker.add(new Gemini())
Broker.add(new GeminiSandbox())
