import { FeatureSource } from "../FeatureSource"
import { Store, UIEventSource } from "../../UIEventSource"
import { Feature, Point } from "geojson"
import { GeoOperations } from "../../GeoOperations"
import { BBox } from "../../BBox"
import { RelationId } from "../../../Models/OsmFeature"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"

export interface SnappingOptions {
    /**
     * If the distance to the line is bigger then this amount, don't snap.
     * In meter
     */
    maxDistance: number

    /**
     * If set to true, no value will be given if no snapping was made
     */
    allowUnsnapped?: false | boolean

    /**
     * The snapped-to way will be written into this
     */
    snappedTo?: UIEventSource<string>

    /**
     * The resulting snap coordinates will be written into this UIEventSource
     */
    snapLocation?: UIEventSource<{ lon: number; lat: number }>

    /**
     * If the projected point is within `reusePointWithin`-meter of an already existing point
     */
    reusePointWithin?: number
}

export default class SnappingFeatureSource
    implements FeatureSource<Feature<Point, { "snapped-to": string; dist: number }>>
{
    public readonly features: Store<[Feature<Point, { "snapped-to": string; dist: number }>]>
    /*Contains the id of the way it snapped to*/
    public readonly snappedTo: Store<string>
    private readonly _snappedTo: UIEventSource<string>

    // private static readonly downloadedRelations: UIEventSource<Map<RelationId, OsmRelation>> = new UIEventSource(new Map())
    private static readonly downloadedRelationMembers: UIEventSource<Feature[]> = new UIEventSource(
        []
    )

    constructor(
        snapTo: FeatureSource,
        location: Store<{ lon: number; lat: number }>,
        options: SnappingOptions
    ) {
        const maxDistance = options?.maxDistance
        this._snappedTo = options.snappedTo ?? new UIEventSource<string>(undefined)
        this.snappedTo = this._snappedTo
        const simplifiedFeatures = snapTo.features
            .mapD((features) =>
                [].concat(
                    ...features
                        .filter((feature) => feature.geometry.type !== "Point")
                        .map((f) => GeoOperations.forceLineString(<any>f))
                )
            )
            .map(
                (features) => {
                    const { lon, lat } = location.data
                    const loc: [number, number] = [lon, lat]
                    return features.filter((f) => BBox.get(f).isNearby(loc, maxDistance))
                },
                [location]
            )

        this.features = location.mapD(
            ({ lon, lat }) => {
                const features = simplifiedFeatures.data.concat(
                    ...SnappingFeatureSource.downloadedRelationMembers.data
                )
                const loc: [number, number] = [lon, lat]
                const maxDistance = (options?.maxDistance ?? 1000) / 1000
                let bestSnap: Feature<Point, { "snapped-to": string; dist: number }> = undefined
                for (const feature of features) {
                    if (feature.geometry.type !== "LineString") {
                        // TODO handle Polygons with holes
                        continue
                    }
                    const snapped: Feature<
                        Point,
                        {
                            dist: number
                            index: number
                            multiFeatureIndex: number
                            location: number
                        }
                    > = GeoOperations.nearestPoint(feature, loc)
                    if (snapped.properties.dist > maxDistance) {
                        continue
                    }
                    if (
                        bestSnap === undefined ||
                        bestSnap.properties.dist > snapped.properties.dist
                    ) {
                        const id = feature.properties.id
                        if (id.startsWith("relation/")) {
                            /**
                             * This is a bit of dirty code:
                             * if we find a relation, we'll start to download it and the members.
                             * The downloaded members will then be used to snap against as well upon a following iteration
                             */
                            SnappingFeatureSource.download(id)
                            continue
                        }
                        bestSnap = {
                            ...snapped,
                            properties: { ...snapped.properties, "snapped-to": id },
                        }
                    }
                }
                this._snappedTo.setData(bestSnap?.properties?.["snapped-to"])
                if (bestSnap === undefined && options?.allowUnsnapped) {
                    bestSnap = {
                        type: "Feature",
                        geometry: {
                            type: "Point",
                            coordinates: [lon, lat],
                        },
                        properties: {
                            "snapped-to": undefined,
                            dist: -1,
                        },
                    }
                }
                const c = bestSnap.geometry.coordinates
                options?.snapLocation?.setData({ lon: c[0], lat: c[1] })
                return [bestSnap]
            },
            [snapTo.features, SnappingFeatureSource.downloadedRelationMembers]
        )
    }

    private static _downloader = new OsmObjectDownloader()
    private static _downloadedRelations = new Set<string>()

    private static async download(id: RelationId) {
        if (SnappingFeatureSource._downloadedRelations.has(id)) {
            return
        }
        SnappingFeatureSource._downloadedRelations.add(id)
        const rel = await SnappingFeatureSource._downloader.DownloadObjectAsync(id, 60 * 24)
        if (rel === "deleted") {
            return
        }
        for (const member of rel.members) {
            if (member.type !== "way") {
                continue
            }
            if (member.role !== "outer" && member.role !== "inner") {
                continue
            }
            const way = await SnappingFeatureSource._downloader.DownloadObjectAsync(
                member.type + "/" + member.ref
            )
            if (way === "deleted") {
                continue
            }
            SnappingFeatureSource.downloadedRelationMembers.data.push(
                ...GeoOperations.forceLineString(way.asGeoJson())
            )
        }
        SnappingFeatureSource.downloadedRelationMembers.ping()
    }
}
