import {Injectable} from '@angular/core';
import {combineLatest, Observable} from 'rxjs';
import {BigNumber, parseUnits} from 'ethers/utils';
import {catchError, map, mergeMap, retry, shareReplay, switchMap} from 'rxjs/operators';
import {fromPromise} from 'rxjs/internal-compatibility';
import {environment} from '../../environments/environment';
import {Web3Service} from './web3.service';
import ChainLinkDaiUsdAggregatorABI from '../abi/ChainLinkDaiUsdAggregatorABI.json';
import {RefreshingReplaySubject} from '../utils';
import {CoinGeckoService} from './coin-gecko.service';
import OracleABI from '../abi/PriceOracleABI.json';
import {getNumerator} from './erc20.helper';

type UsdPriceCache = { [tokenAddress: string]: RefreshingReplaySubject<BigNumber> };

@Injectable({
    providedIn: 'root'
})
export class TokenPriceService {

    private usdPriceCache$: UsdPriceCache = {};

    public ethUsdPriceBN$: Observable<BigNumber> = this.getChainLinkAggregatorInstance(
        environment.ETH_USD_CHAINLINK_ORACLE_CONTRACT_ADDRESS
    ).pipe(
        mergeMap((instance) => {

            // @ts-ignore
            const call$ = instance.methods.latestAnswer().call();
            return fromPromise(call$) as Observable<BigNumber>;
        }),
        retry(5),
        shareReplay({bufferSize: 1, refCount: true})
    );

    constructor(
        private web3Service: Web3Service,
        private coinGeckoService: CoinGeckoService
    ) {
    }

    public getUsdTokenPrice(
        tokenAddress: string,
        tokenDecimals: number,
        blockNumber?: number | 'latest',
    ): Observable<BigNumber> {

        if (this.usdPriceCache$[tokenAddress]) {
            return this.usdPriceCache$[tokenAddress];
        }

        const tokenPrice$ = this.getTokenPriceBN(
            tokenAddress,
            tokenDecimals,
            blockNumber,
        );

        this.usdPriceCache$[tokenAddress] = new RefreshingReplaySubject<BigNumber>(
            () => tokenPrice$,
            10000
        );

        return this.usdPriceCache$[tokenAddress];
    }

    public getTokenPriceBN(
        tokenAddress: string,
        tokenDecimals: number,
        blockNumber?: number | 'latest',
        useOffChain = true
    ): Observable<BigNumber> {

        const onChainPrice$ = combineLatest([
            this.ethUsdPriceBN$,
            this.getEthTokenPriceFromOracle(tokenAddress, tokenDecimals)
        ]).pipe(
          map(([ethUsdPrice, ethPrice]) => {
              return ethPrice.mul(ethUsdPrice).div(getNumerator(18));
          })
        );

        if (!useOffChain) {
            return onChainPrice$;
        }

        const offChainPrice$ = this.coinGeckoService.getUSDPrice(tokenAddress).pipe(
            map((price: number) => parseUnits(String(price), 8)),
        );

        return offChainPrice$.pipe(
            catchError(() => onChainPrice$)
        );
    }

    private getEthTokenPriceFromOracle(tokenAddress: string, decimals: number): Observable<BigNumber> {
        return this.web3Service.getInstance(OracleABI, environment.PRICE_ORACLE_CONTRACT).pipe(
            switchMap((instance) => {
                const call$ = instance.methods.getRate(
                    tokenAddress,
                    '0x0000000000000000000000000000000000000000'
                ).call();
                return fromPromise(call$);
            }),
            map((res: BigNumber) => {
                return res.mul(getNumerator(decimals)).div(getNumerator(18));
            })
        );
    }

    private getChainLinkAggregatorInstance(aggregatorAddress: string) {
        return this.web3Service.getInstance(
            ChainLinkDaiUsdAggregatorABI,
            aggregatorAddress
        );
    }
}