import OsmChangeAction from "./OsmChangeAction"
import { Changes } from "../Changes"
import { ChangeDescription } from "./ChangeDescription"
import { OsmRelation, OsmWay } from "../OsmObject"
import OsmObjectDownloader from "../OsmObjectDownloader"

export interface RelationSplitInput {
    relation: OsmRelation
    originalWayId: number
    allWayIdsInOrder: number[]
    originalNodes: number[]
    allWaysNodesInOrder: number[][]
}

abstract class AbstractRelationSplitHandler extends OsmChangeAction {
    protected readonly _input: RelationSplitInput
    protected readonly _theme: string
    protected readonly _objectDownloader: OsmObjectDownloader

    constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) {
        super("relation/" + input.relation.id, false)
        this._input = input
        this._theme = theme
        this._objectDownloader = objectDownloader
    }

    /**
     * Returns which node should border the member at the given index
     */
    protected async targetNodeAt(i: number, first: boolean) {
        const member = this._input.relation.members[i]
        if (member === undefined) {
            return undefined
        }
        if (member.type === "node") {
            return member.ref
        }
        if (member.type === "way") {
            const osmWay = <OsmWay>(
                await this._objectDownloader.DownloadObjectAsync("way/" + member.ref)
            )
            const nodes = osmWay.nodes
            if (first) {
                return nodes[0]
            } else {
                return nodes[nodes.length - 1]
            }
        }
        if (member.type === "relation") {
            return undefined
        }
        return undefined
    }
}

/**
 * When a way is split and this way is part of a relation, the relation should be updated too to have the new segment if relevant.
 */
export default class RelationSplitHandler extends AbstractRelationSplitHandler {
    constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) {
        super(input, theme, objectDownloader)
    }

    async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
        if (this._input.relation.tags["type"] === "restriction") {
            // This is a turn restriction
            return new TurnRestrictionRSH(
                this._input,
                this._theme,
                this._objectDownloader
            ).CreateChangeDescriptions(changes)
        }
        return new InPlaceReplacedmentRTSH(
            this._input,
            this._theme,
            this._objectDownloader
        ).CreateChangeDescriptions(changes)
    }
}

export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
    constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) {
        super(input, theme, objectDownloader)
    }

    public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
        const relation = this._input.relation
        const members = relation.members

        const selfMembers = members.filter(
            (m) => m.type === "way" && m.ref === this._input.originalWayId
        )

        if (selfMembers.length > 1) {
            console.warn(
                "Detected a turn restriction where this way has multiple occurances. This is an error"
            )
        }
        const selfMember = selfMembers[0]

        if (selfMember.role === "via") {
            // A via way can be replaced in place
            return new InPlaceReplacedmentRTSH(
                this._input,
                this._theme,
                this._objectDownloader
            ).CreateChangeDescriptions(changes)
        }

        // We have to keep only the way with a common point with the rest of the relation
        // Let's figure out which member is neighbouring our way

        let commonStartPoint: number = await this.targetNodeAt(members.indexOf(selfMember), true)
        let commonEndPoint: number = await this.targetNodeAt(members.indexOf(selfMember), false)

        // In normal circumstances, only one of those should be defined
        let commonPoint = commonStartPoint ?? commonEndPoint

        // Let's select the way to keep
        const idToKeep: { id: number } = this._input.allWaysNodesInOrder
            .map((nodes, i) => ({
                nodes: nodes,
                id: this._input.allWayIdsInOrder[i],
            }))
            .filter((nodesId) => {
                const nds = nodesId.nodes
                return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint
            })[0]

        if (idToKeep === undefined) {
            console.error("No common point found, this was a broken turn restriction!", relation.id)
            return []
        }

        const originalWayId = this._input.originalWayId
        if (idToKeep.id === originalWayId) {
            console.log("Turn_restriction fixer: the original ID can be kept, nothing to do")
            return []
        }

        const newMembers: {
            ref: number
            type: "way" | "node" | "relation"
            role: string
        }[] = relation.members.map((m) => {
            if (m.type === "way" && m.ref === originalWayId) {
                return {
                    ref: idToKeep.id,
                    type: "way",
                    role: m.role,
                }
            }
            return m
        })

        return [
            {
                type: "relation",
                id: relation.id,
                changes: {
                    members: newMembers,
                },
                meta: {
                    theme: this._theme,
                    changeType: "relation-fix:turn_restriction",
                },
            },
        ]
    }
}

/**
 * A simple strategy to split relations:
 * -> Download the way members just before and just after the original way
 * -> Make sure they are still aligned
 *
 * Note that the feature might appear multiple times.
 */
export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
    constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) {
        super(input, theme, objectDownloader)
    }

    async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
        const wayId = this._input.originalWayId
        const relation = this._input.relation
        const members = relation.members
        const originalNodes = this._input.originalNodes
        const firstNode = originalNodes[0]
        const lastNode = originalNodes[originalNodes.length - 1]
        const newMembers: { type: "node" | "way" | "relation"; ref: number; role: string }[] = []

        for (let i = 0; i < members.length; i++) {
            const member = members[i]
            if (member.type !== "way" || member.ref !== wayId) {
                newMembers.push(member)
                continue
            }

            const nodeIdBefore = await this.targetNodeAt(i - 1, false)
            const nodeIdAfter = await this.targetNodeAt(i + 1, true)

            const firstNodeMatches = nodeIdBefore === undefined || nodeIdBefore === firstNode
            const lastNodeMatches = nodeIdAfter === undefined || nodeIdAfter === lastNode

            if (firstNodeMatches && lastNodeMatches) {
                // We have a classic situation, forward situation
                for (const wId of this._input.allWayIdsInOrder) {
                    newMembers.push({
                        ref: wId,
                        type: "way",
                        role: member.role,
                    })
                }
                continue
            }

            const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode
            const lastNodeMatchesRev = nodeIdAfter === undefined || nodeIdAfter === firstNode
            if (firstNodeMatchesRev || lastNodeMatchesRev) {
                // We (probably) have a reversed situation, backward situation
                for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--) {
                    // Iterate BACKWARDS
                    const wId = this._input.allWayIdsInOrder[i1]
                    newMembers.push({
                        ref: wId,
                        type: "way",
                        role: member.role,
                    })
                }
                continue
            }

            // Euhm, allright... Something weird is going on, but let's not care too much
            // Lets pretend this is forward going
            for (const wId of this._input.allWayIdsInOrder) {
                newMembers.push({
                    ref: wId,
                    type: "way",
                    role: member.role,
                })
            }
        }

        return [
            {
                id: relation.id,
                type: "relation",
                changes: { members: newMembers },
                meta: {
                    changeType: "relation-fix",
                    theme: this._theme,
                },
            },
        ]
    }
}