import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, } from 'rxjs';
import { map, } from 'rxjs/operators';
import neo4j, {
    isDuration,
    isLocalTime,
    isTime,
    isDate,
    isDateTime,
    isLocalDateTime,
    isInt,
    isPoint,
 } from 'neo4j-driver'
import { isNode, isRelationship } from 'neo4j-driver-core/lib/graph-types'
import Result from 'neo4j-driver-core/lib/result'

const toNative = (value: any, showLabelsOrType?: boolean, showIdentity?: boolean): any => {
    if ( value === null || value === undefined ) return undefined
    else if ( value instanceof Result || value.records ) {
        return value.records.map(row => Object.fromEntries(
            row.keys.map(key => [ key, toNative(row.get(key)) ])
        ))
    }
    else if ( Array.isArray(value) ) return value.map(value => toNative(value))
    else if ( isNode(value) ) return toNative({
        _id: showIdentity ?  toNative(value.identity) : null,
        _labels: showLabelsOrType ? toNative(value.labels) : null,
        ...toNative(value.properties),
    })
    else if ( isRelationship(value) ) return toNative({
        _id: toNative(value.identity),
        _type: showLabelsOrType? toNative(value.type) : null,
        ...toNative(value.properties),
    })
    // Number
    else if ( isInt(value) ) return value.toNumber()

    // Temporal
    else if (
        isDuration(value)  ||
        isLocalTime(value) ||
        isTime(value) ||
        isDate(value) ||
        isDateTime(value) ||
        isLocalDateTime(value)
    ) {
        return value.toString()
    }

    // Spatial
    if ( isPoint(value) ) {
        switch (value.srid.toNumber()) {
            case 4326:
                return { longitude: value.y, latitude: value.x }

            case 4979:
                return { longitude: value.y, latitude: value.x, height: value.z }

            default:
                return toNative({ x: value.x, y: value.y, z: value.z })
        }

    }

    // Object
    else if ( typeof value === 'object' ) {
        return Object.fromEntries(

            Object.keys(value).map(key => [key, toNative(value[ key ], showLabelsOrType, showIdentity)])
        )
    }

    return value
}

@Injectable()
export class Neo4jTypeInterceptor implements NestInterceptor {

    private showLabelsOrType: boolean = false;
    private showIdentity: boolean = false;

    constructor() {}

    setOptions(showLabelsOrType: boolean = false, showIdentity: boolean = false) {
        this.showLabelsOrType = showLabelsOrType
        this.showIdentity = showIdentity

        return this
    }

    static withOptions(showLabelsOrType: boolean = false, showIdentity: boolean = false): NestInterceptor {
        return (new Neo4jTypeInterceptor()).setOptions(showLabelsOrType, showIdentity)
    }

    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next.handle()
            .pipe(
                map(data => toNative(data, this.showLabelsOrType, this.showIdentity))
            );
    }
}
