/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as idl from './idl'

export interface IDLLibrary {
    readonly files: readonly idl.IDLFile[]
}

function createLibrary(files:idl.IDLFile[]): IDLLibrary {
    return {
        files
    }
}

function toLibrary(ii:Iterable<idl.IDLFile>): IDLLibrary {
    return {
        files: Array.from(ii)
    }
}

function serializeParam(params:unknown): string {
    if (typeof params === 'undefined') {
        return 'undefined'
    }
    if (typeof params === 'boolean') {
        return params ? 'true' : 'false'
    }
    if (typeof params === 'string') {
        return `"${params}"`
    }
    if (typeof params === 'number') {
        return params.toString()
    }
    if (typeof params === 'object') {
        if (params === null) {
            return 'null'
        }
        if (Array.isArray(params)) {
            return '[' + params.map(serializeParam).join(', ') + ']'
        }
        const keys = Object.keys(params)
        keys.sort()
        return '{' + 
            keys.map((key:string):string => {
                if (typeof key !== 'string') {
                    throw new Error(`Unsupported key! "${typeof key}"`) 
                }
                const objectKey = key as keyof typeof params
                return `${key}=${serializeParam(params[objectKey])}`
            }).join(',') 
        + '}'
    }
    throw new Error(`Unsupported type! "${typeof params}"`)
}

const queryCache = new Map<string, any>()
function cached<A, R>(key: string, f:(x:A) => R): (x:A) => R {
    return x => {
        if (queryCache.has(key)) {
            return queryCache.get(key)!
        }
        const v = f(x)
        queryCache.set(key, v)
        return v
    }
}

export interface LibraryQuery<A, R> {
    fn: (x:A) => R
    key: string
    _reqBrand: unknown
}
export interface LibraryReducer<R> {
    fn: (x:IDLLibrary) => R
    key: string
    _redBrand: unknown
}

function reduce<R>(key:string, f:(x:IDLLibrary) => R): LibraryReducer<R> {
    return {
        fn: cached(key, f),
        key,
        _redBrand: {}
    }
}

function req<A, R>(key:string, fn:(x:A) => R): LibraryQuery<A, R> {
    return {
        fn,
        key,
        _reqBrand: {}
    }
}

function compose<B, R>(base:LibraryReducer<B>, next: LibraryQuery<B, R>): LibraryReducer<R> {
    const key = base.key + '.' + next.key
    return {
        fn: cached(key, x => next.fn(base.fn(x))),
        key,
        _redBrand: {}
    }
}

function concat<A, R1, R2>(f:LibraryQuery<A, R1>, g:LibraryQuery<A, R2>): LibraryQuery<A, [R1, R2]> {
    const key = `$pair{${f.key},${g.key}}`
    return {
        fn: cached(key, x => {
            const r1 = f.fn(x)
            const r2 = g.fn(x)
            return [r1, r2]
        }),
        key,
        _reqBrand: {}
    }
}

class LensBuilder<R> {
    private constructor(
        public req: LibraryReducer<R>
    ) {}

    static make<R>(r:LibraryReducer<R>) {
        return new LensBuilder<R>(r)
    }

    pipe<T>(r:LibraryQuery<R, T>): LensBuilder<T> {
        return new LensBuilder(
            compose(this.req, r)
        )
    }

    row<T>(key:string, f:(x:R) => T): LensBuilder<T> {
        return this.pipe(req(key, f))
    }

    query(): LibraryReducer<R> {
        return this.req
    }
}

export function lens<R>(r:LibraryReducer<R>): LensBuilder<R> {
    return LensBuilder.make(r)
}

export type QueryType<T> = LibraryQuery<IDLLibrary, T> | LensBuilder<T>

export function query<T>(lib:IDLLibrary, input:QueryType<T>): T {
    const request = input instanceof LensBuilder ? input.query() : input
    return request.fn(lib)
}

// UTILS

const utils = {
    idx: <T>(x:number) => req<T[], T | undefined>('idx', xs => xs.at(x)),
    fst: <T>() => req<T[], T | undefined>('fst', xs => xs.at(0)),
    lst: <T>() => req<T[], T | undefined>('lst', xs => xs.at(-1)), 
}

interface EntitiesParams {
    expandNamespaces?: boolean
}

const select = {
    files(): LibraryReducer<readonly idl.IDLFile[]> {
        return reduce('files', x => x.files)
    },
    nodes(params?:EntitiesParams): LibraryQuery<readonly idl.IDLFile[], idl.IDLNode[]> {
        const key = 'entities' + serializeParam(params)
        function go(node:idl.IDLNode): idl.IDLNode[] {
            if (idl.isNamespace(node) && params?.expandNamespaces) {
                return node.members.flatMap(go)
            }
            return [node]
        }
        return req(key, xs => {
            return xs.flatMap(x => x.entries).flatMap(go)
        })
    },
    entries(): LibraryQuery<idl.IDLNode[], idl.IDLEntry[]> {
        return req('entries', xs => xs.filter(idl.isEntry))
    },
    interfaces(): LibraryQuery<idl.IDLNode[], idl.IDLInterface[]> {
        return req('interfaces', it => it.filter(idl.isInterface))
    },
    hasExt<T extends idl.IDLNode>(attr:idl.IDLExtendedAttributes): LibraryQuery<T[], T[]> {
        return req('with_attr=' + serializeParam(attr), it => it.filter(x => idl.hasExtAttribute(x, attr)))
    },
    names(): LibraryQuery<idl.IDLNode[], string[]> {
        return req('names', xs => xs.flatMap(x => idl.isNamedNode(x) ? [x.name] : []))
    },
    name(name:string): LibraryReducer<idl.IDLNode[]> {
        return reduce(`select.by.name.${name}`, lib => {
            return lib.files.flatMap(it => {
                return it.entries.flatMap(it => {
                    if (idl.isNamedNode(it) && it.name === name) {
                        return [it]
                    }
                    return []
                })
            })
        })   
    },
}

export const lib = {
    createLibrary,
    toLibrary,
    
    lens,
    query,

    select,
    utils,

    req,
    compose,
    concat,

    other: {
        serializeParam
    }
}