export const NATIVE_TOKEN = '0x0000000000000000000000000000000000000001'
export const USD_FIAT = '0x0000000000000000000000000000000000000055'  // We use 0x55 (ASCII 'U') to indicate the use of fiat USD

export function mixin(child, ...parents) {
    // child is modified directly, assigning fields from parents that are missing in child.  parents fields are
    // assigned by parents order, highest priority first
    for( const parent of parents ) {
        for ( const key in parent) {
            if (parent.hasOwnProperty(key) && !child.hasOwnProperty(key) && parent[key] !== undefined) {
                child[key] = parent[key];
            }
        }
    }
    return child;
}

export function prototype(parent, child) {
    const result = Object.create(parent);
    Object.assign(result, child)
    return result
}

export function invokeCallback(cbs, prop, ...args) {
    if (prop in cbs) cbs[prop].call(cbs, ...args)
}

export function invokeCallbacks(callbacks, prop, ...args) {
    if (!callbacks) return
    callbacks.forEach((cb)=>invokeCallback(cb, prop, ...args))
}


export function encodeIEE754(value) {
    const buffer = new ArrayBuffer(4);
    const view = new DataView(buffer);
    view.setFloat32(0, value, false /* big endian */);
    return view.getUint32(0, false);
}


export function decodeIEE754(value) {
    const buffer = new ArrayBuffer(4);
    const view = new DataView(buffer);
    view.setUint32(0, value, false)
    return view.getFloat32(0, false)
}


export function buildMetadataMap(metadata) {
    const metadataMap = {}
    for (const [chain, info] of Object.entries(metadata)) {
        const map = {}
        for (const poolMeta of info.p)
            map[poolMeta.a] = poolMeta
        for (const tokenMeta of info.t)
            map[tokenMeta.a] = tokenMeta
        metadataMap[Number(chain)] = map
    }
    // console.log('metadataMap', metadataMap)
    return metadataMap
}


//
// AsyncCache
//

export class AsyncCache {
    // fetch(key) returns a value
    constructor(fetch) {
        this.cache = {}
        this.fetchLocks = {}
        this.fetch = fetch
    }

    async get(key) {
        if (this.cache[key]) {
            return this.cache[key]
        }
        if (this.fetchLocks[key]) {
            return await this.fetchLocks[key]
        }
        const fetchPromise = this.fetch(key)
        this.fetchLocks[key] = fetchPromise
        const result = await fetchPromise
        this.cache[key] = result
        delete this.fetchLocks[key]
        return result
    }
}


export class AsyncAbiCache extends AsyncCache {
    constructor(fetch) {
        super(async (key)=>{
            const result = await fetch(key)
            return result.abi
        });
    }
}


export class AsyncURLCache extends AsyncAbiCache {
    constructor(urlForKey) {
        super(async (key) => {
            const URL = this.urlForKey(key)
            const response = await fetch(URL)
            if (!response.ok)
                throw new Error(`Could not fetch ${URL} (status ${response.status})`)
            return await response.json()
        })
        this.urlForKey = urlForKey
    }
}


export class AbiURLCache extends AsyncURLCache {
    constructor(baseUrl) {
        super((name)=>{
            return this.baseUrl+abiPath(name)
        })
        this.baseUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'
    }
}


const files = {
    // If a contract is in a file different than its name, put the exception here
    // 'IVaultImpl' : 'IVault',  // for example
    // 'IERC20Metadata' : 'interfaces/IERC20Metadata',
}

export function abiPath(name) {
    const file = files[name]
    return `${file??name}.sol/${name}.json`
}


export function parseFill(obj) {
    let [tx, time, filledIn, filledOut, fee] = obj
    time = new Date(time * 1000)
    filledIn = BigInt(filledIn)
    filledOut = BigInt(filledOut)
    fee = BigInt(fee)
    return {tx, time, filledIn, filledOut, fee}
}

export function timestamp(date = null) {
    if (date === null)
        date = new Date()
    return Math.round(date.getTime() / 1000)
}

export function dateString(datetime) {
    return datetime.toLocaleString({dateStyle: 'medium', timeStyle: 'short'})
}

export function logicalXOR(a, b) {
    return (a || b) && !(a && b)
}

// Base62

// base62.js
const base62charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

export function encodeBase62(num) {
    if (num === 0) return base62charset[0];
    let encoded = "";
    while (num > 0) {
        encoded = base62charset[num % 62] + encoded;
        num = Math.floor(num / 62);
    }
    return encoded;
}

export function decodeBase62(str) {
    return str.split('').reverse().reduce((acc, char, i) =>
        acc + base62charset.indexOf(char) * Math.pow(62, i), 0);
}


export function withTimeout(promise, timeoutDuration, fallbackErrorMessage) {
    return Promise.race([
        promise,
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error(fallbackErrorMessage)), timeoutDuration)
        ),
    ]);
}
