import type { coreGraph } from '../core/graph';
import type { Action } from '../core/history';
import type { osmWay, RelationId } from '../osm';
import { osmJoinWays } from '../osm/multipolygon';
import type { osmRelation, RelationMember } from '../osm/relation';
import { utilArrayGroupBy, utilObjectOmit } from '../util';

interface MemberWithIndex extends RelationMember { index?: number };

export function actionAddMember(relationId: RelationId, member: RelationMember, memberIndex: number): Action {

    return function action(graph) {
        var relation = graph.entity(relationId);

        // There are some special rules for Public Transport v2 routes.
        var isPTv2 = /stop|platform/.test(member.role);

        if (member.type === 'way' && !isPTv2) {
            // Try to perform sensible inserts based on how the ways join together
            graph = addWayMember(relation, graph);
        } else {
            // see https://wiki.openstreetmap.org/wiki/Public_transport#Service_routes
            // Stops and Platforms for PTv2 should be ordered first.
            // hack: We do not currently have the ability to place them in the exactly correct order.
            if (isPTv2 && isNaN(memberIndex)) {
                memberIndex = 0;
            }

            graph = graph.replace(relation.addMember(member, memberIndex));
        }

        return graph;
    };


    // Add a way member into the relation "wherever it makes sense".
    function addWayMember(relation: osmRelation, graph: coreGraph) {
        var groups, item, i, j, k;

        // remove PTv2 stops and platforms before doing anything.
        var PTv2members = [];
        var members: MemberWithIndex[] = [];
        for (i = 0; i < relation.members.length; i++) {
            var m = relation.members[i];
            if (/stop|platform/.test(m.role)) {
                PTv2members.push(m);
            } else {
                members.push(m);
            }
        }
        relation = relation.update({ members: members });

        // Add the member anywhere, one time. Just push and let `osmJoinWays` decide where to put it.
        groups = utilArrayGroupBy(relation.members, 'type');
        groups.way = groups.way || [];
        groups.way.push(member);

        members = withIndex(groups.way);
        var joined = osmJoinWays(members, graph);

        // `joined` might not contain all of the way members,
        // But will contain only the completed (downloaded) members
        for (i = 0; i < joined.length; i++) {
            var segment = joined[i];
            var nodes = segment.nodes.slice();
            var startIndex = segment[0].index;

            // j = array index in `members` where this segment starts
            for (j = 0; j < members.length; j++) {
                if (members[j].index === startIndex) {
                    break;
                }
            }

            // k = each member in segment
            for (k = 0; k < segment.length; k++) {
                item = segment[k];
                var way = graph.entity<osmWay>(item.id);

                // reorder `members` if necessary
                if (k > 0) {
                    if (j+k >= members.length || item.index !== members[j+k].index) {
                        moveMember(members, item.index!, j+k);
                    }
                }

                nodes.splice(0, way.nodes.length - 1);
            }
        }

        // Final pass: skip dead items, remove index properties
        var wayMembers: RelationMember[] = [];
        for (i = 0; i < members.length; i++) {
            item = members[i];
            if (item.index === -1) continue;

            wayMembers.push(utilObjectOmit(item, ['index']));
        }

        // Put stops and platforms first, then nodes, ways, relations
        // This is recommended for Public Transport v2 routes:
        // see https://wiki.openstreetmap.org/wiki/Public_transport#Service_routes
        var newMembers = PTv2members.concat( (groups.node || []), wayMembers, (groups.relation || []) );

        return graph.replace(relation.update({ members: newMembers }));


        // `moveMember()` changes the `members` array in place by splicing
        // the item with `.index = findIndex` to where it belongs,
        // and marking the old position as "dead" with `.index = -1`
        //
        // j=5, k=0                jk
        // segment                 5 4 7 6
        // members       0 1 2 3 4 5 6 7 8 9        keep 5 in j+k
        //
        // j=5, k=1                j k
        // segment                 5 4 7 6
        // members       0 1 2 3 4 5 6 7 8 9        move 4 to j+k
        // members       0 1 2 3 x 5 4 6 7 8 9      moved
        //
        // j=5, k=2                j   k
        // segment                 5 4 7 6
        // members       0 1 2 3 x 5 4 6 7 8 9      move 7 to j+k
        // members       0 1 2 3 x 5 4 7 6 x 8 9    moved
        //
        // j=5, k=3                j     k
        // segment                 5 4 7 6
        // members       0 1 2 3 x 5 4 7 6 x 8 9    keep 6 in j+k
        //
        function moveMember(arr: MemberWithIndex[], findIndex: number, toIndex: number) {
            var i;
            for (i = 0; i < arr.length; i++) {
                if (arr[i].index === findIndex) {
                    break;
                }
            }

            var item = Object.assign({}, arr[i]);   // shallow copy
            arr[i].index = -1; // mark previous entry as dead
            delete item.index; // inserted items must never be moved again
            arr.splice(toIndex, 0, item);
        }


        // This is the same as `Relation.indexedMembers`,
        // Except we don't want to index all the members, only the ways
        function withIndex(arr: RelationMember[]) {
            var result = new Array(arr.length);
            for (var i = 0; i < arr.length; i++) {
                result[i] = Object.assign({}, arr[i]);   // shallow copy
                result[i].index = i;
            }
            return result;
        }
    }

}
