#!/usr/bin/env python
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
# Copyright (C) 2010-2026 German Aerospace Center (DLR) and others.
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0/
# This Source Code may also be made available under the following Secondary
# Licenses when the conditions for such availability set forth in the Eclipse
# Public License 2.0 are satisfied: GNU General Public License, version 2
# or later which is available at
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later

# @file    addStops2Routes.py
# @author  Yun-Pang Floetteroed
# @author  Jakob Erdmann
# @date    2019-04-25

"""
add stops to vehicle routes
"""
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
import random
from collections import defaultdict

sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import sumolib  # noqa


def get_options(args=None):
    op = sumolib.options.ArgumentParser()
    op.add_option("-n", "--net-file", category='input', dest="netfile", type=op.net_file, required=True,
                  help="define the net filename (mandatory)")
    op.add_option("-r", "--route-files", category='input', dest="routefiles", type=op.route_file,
                  help="define the route files separated by comma (mandatory)")
    op.add_option("-o", "--output-file", category='output', dest="outfile", type=op.route_file,
                  help="define the output filename")
    op.add_option("-t", "--typesfile", category='input', dest="typesfile",
                  help="Give a typesfile")
    op.add_option("-d", "--duration",
                  help="Define duration of vehicle stop (setting 'X-Y' picks randomly from [X,Y[)")
    op.add_option("-u", "--until",
                  help="Define end time of vehicle stop")
    op.add_option("--speed",
                  help="Define a waypoint with the given maximum speed")
    op.add_option("-p", "--parking", dest="parking", action="store_true",
                  default=False, help="Let the vehicle stop beside the road")
    op.add_option("--length", type=float,
                  help="length of the stopping position")
    op.add_option("--relpos",
                  help="relative stopping position along the edge [0,1] or 'random'")
    op.add_option("--edges",
                  help="comma separated list of edges to consider for stopping")
    op.add_option("--color",
                  help="change color of stopping vehicles")
    op.add_option("--lane", default="0",
                  help="set index of stop lane or 'random' (unusable lanes are not counted)")
    op.add_option("--reledge", default="1",
                  help="relative stopping position along the route [0,1] or 'random' (1 indicates the last edge)")
    op.add_option("--via-index", dest="viaIndex",
                  help="index of stop edge along the route (0-based, negative allowed) or 'random'")
    op.add_option("--probability", type=float, default=1,
                  help="add stop with the given probability ]0, 1]")
    op.add_option("--parking-areas", default=False, type=op.additional_file,
                  help="load parkingarea definitions and stop at parkingarea on the arrival edge if possible")
    op.add_option("--start-at-stop", action="store_true",
                  default=False, help="shorten route so it starts at stop")
    op.add_option("--rel-occupancy", type=float,
                  help="fill all parkingAreas to relative occupancy [0,1]")
    op.add_option("--abs-occupancy", type=int, default=1,
                  help="fill all parkingAreas to absolute occupancy")
    op.add_option("--abs-free", type=int,
                  help="fill all parkingAreas to absolute remaining capacity")
    op.add_option("-D", "--person-duration", dest="pDuration",
                  help="Define duration of person stop (setting 'X-Y' picks randomly from [X,Y[)")
    op.add_option("-U", "--person-until", dest="pUntil",
                  help="Define end time of person stop")
    op.add_option("-s", "--seed", type=int, default=42, help="random seed")
    op.add_option("-v", "--verbose", dest="verbose", action="store_true",
                  default=False, help="tell me what you are doing")

    options = op.parse_args(args)

    if options.edges:
        options.edges = options.edges.split(",")
    if options.parking_areas:
        options.parking_areas = options.parking_areas.split(",")

    if not options.routefiles:
        if options.parking_areas:
            options.start_at_stop = True
            options.routefiles = []
            if not options.outfile:
                options.outfile = options.parking_areas[0][:-4] + ".stops.xml"
        else:
            sys.exit("--parking-areas needed to generation stationary traffic without route-files")
    else:
        options.routefiles = options.routefiles.split(',')
        if not options.outfile:
            options.outfile = options.routefiles[0][:-4] + ".stops.xml"

    if not options.typesfile:
        options.typesfile = options.routefiles
    else:
        options.typesfile = options.typesfile.split(",")

    if not options.duration and not options.until and not options.speed:
        op.print_help()
        sys.exit("stop duration, speed or until missing")

    if options.relpos is not None:
        try:
            options.relpos = float(options.relpos)
            if options.relpos < 0 or options.relpos > 1:
                raise ValueError
        except ValueError:
            if options.relpos != 'random':
                sys.exit("option --relpos must be set to 'random' or to a float value from [0,1]")
    if options.lane is not None:
        try:
            options.lane = int(options.lane)
            if options.lane < 0:
                raise ValueError
        except ValueError:
            if options.lane != 'random':
                sys.exit("option --lane must be set to 'random' or to a non-negative integer value")
    if options.reledge is not None:
        try:
            options.reledge = float(options.reledge)
            if options.reledge < 0 or options.reledge > 1:
                raise ValueError
        except ValueError:
            if options.reledge != 'random':
                sys.exit("option --reledge must be set to 'random' or to a float value from [0,1]")
    if options.viaIndex is not None:
        try:
            options.viaIndex = int(options.viaIndex)
        except ValueError:
            if options.viaIndex != 'random':
                sys.exit("option --via-index must be set to 'random' or to an integer value")

    return options


def readTypes(options):
    vtypes = {None: "passenger"}
    for file in options.typesfile:
        for vtype in sumolib.xml.parse(file, 'vType'):
            vtypes[vtype.id] = vtype.getAttributeSecure("vClass", "passenger")
    # print(vtypes)
    return vtypes


def getEdgeIDs(obj):
    result = []
    if obj.route:
        return obj.route[0].edges.split()
    if obj.attr_from:
        result.append(obj.attr_from)
    via = getattr(obj, "via", None)
    if via:
        result += via.split()
    if obj.to:
        result.append(obj.to)
    return result


def interpretDuration(duration):
    if '-' in duration:
        start, stop = duration.split('-')
        return random.randrange(int(start), int(stop))
    else:
        return duration


def loadRouteFiles(options, routefile, edge2parking, outf):
    net = sumolib.net.readNet(options.netfile)
    vtypes = readTypes(options)
    numSkipped = defaultdict(lambda: 0)

    if options.viaIndex is not None and options.reledge not in (None, 1, 1.0):
        print("Warning: --via-index overrides --reledge", file=sys.stderr)

    for routefile in options.routefiles:
        for obj in sumolib.xml.parse(routefile, ['vehicle', 'trip', 'flow', 'person', 'vType']):
            if obj.name == 'vType':
                outf.write(obj.toXML(' '*4))
                continue
            edgeIDs = getEdgeIDs(obj)
            stopEdgeID = None
            if edgeIDs:
                if options.viaIndex is not None:
                    if options.viaIndex == 'random':
                        edgeIndex = random.randrange(len(edgeIDs))
                    else:
                        viaIndex = options.viaIndex
                        if viaIndex < -len(edgeIDs) or viaIndex >= len(edgeIDs):
                            numSkipped[obj.name] += 1
                            print("Warning: via-index %s out of range for %s '%s' (route length %s); no stop added" % (
                                options.viaIndex, obj.name, obj.id, len(edgeIDs)), file=sys.stderr)
                            outf.write(obj.toXML(' '*4))
                            continue
                        edgeIndex = viaIndex if viaIndex >= 0 else len(edgeIDs) + viaIndex
                    stopEdgeID = edgeIDs[edgeIndex]
                elif options.edges:
                    for e in options.edges:
                        if e in edgeIDs:
                            stopEdgeID = e
                            break
                else:
                    reledge = options.reledge
                    if reledge == 'random':
                        reledge = random.random()
                    stopEdgeID = edgeIDs[int(round(reledge * (len(edgeIDs) - 1)))]
            if ((stopEdgeID is not None or obj.name == 'person') and
                    options.probability < 1 and random.random() > options.probability):
                outf.write(obj.toXML(' '*4))
                continue

            if stopEdgeID is None:
                if obj.name == 'person' and (
                        options.pDuration is not None
                        or options.pUntil is not None):
                    stopAttrs = {}
                    if options.pDuration:
                        stopAttrs["duration"] = interpretDuration(options.pDuration)
                    if options.pUntil:
                        stopAttrs["until"] = options.pUntil
                    # stop location is derived automatically from previous plan element
                    obj.addChild("stop", attrs=stopAttrs)
                else:
                    numSkipped[obj.name] += 1
                outf.write(obj.toXML(' '*4))
                continue

            try:
                stopEdge = net.getEdge(stopEdgeID)
            except Exception:
                numSkipped[obj.name] += 1
                print("Warning: edge '%s' not found in net for %s '%s'; no stop added" % (
                    stopEdgeID, obj.name, obj.id), file=sys.stderr)
                outf.write(obj.toXML(' '*4))
                continue
            skip = False
            stopAttrs = {}
            if options.parking_areas:
                if stopEdgeID in edge2parking:
                    stopAttrs["parkingArea"] = edge2parking[stopEdgeID]
                else:
                    skip = True
                    numSkipped[obj.name] += 1
                    print("Warning: no parkingArea found on edge '%s' for vehicle '%s'" % (
                        stopEdgeID, obj.id), file=sys.stderr)
            else:
                # find usable lane
                skip = True
                lanes = stopEdge.getLanes()
                usable = [lane for lane in lanes if lane.allows(vtypes[obj.type])]
                if usable:
                    lane = None
                    if options.lane == 'random':
                        lane = random.choice(usable)
                    elif options.lane < len(usable):
                        lane = usable[options.lane]
                    if lane:
                        skip = False
                        stopAttrs["lane"] = lane.getID()
                        endPos = lane.getLength()
                        if options.relpos:
                            relpos = options.relpos
                            if options.relpos == 'random':
                                relpos = random.random()
                            endPos *= relpos
                            stopAttrs["endPos"] = "%.2f" % endPos
                        if options.length:
                            stopAttrs["startPos"] = "%.2f" % (endPos - options.length)
                            stopAttrs["friendlyPos"] = "true"
                if skip:
                    numSkipped[obj.name] += 1
                    print("Warning: no allowed lane found on edge '%s' for vehicle '%s' (%s)" % (
                        stopEdgeID, obj.id, vtypes[obj.type]), file=sys.stderr)

            if options.parking:
                stopAttrs["parking"] = "true"
            if options.duration:
                stopAttrs["duration"] = interpretDuration(options.duration)
            if options.speed:
                stopAttrs["speed"] = interpretDuration(options.speed)
            if options.until:
                stopAttrs["until"] = interpretDuration(options.until)
            if not skip:
                obj.addChild("stop", attrs=stopAttrs)
                if options.start_at_stop:
                    obj.setAttribute("departPos", "stop")
                    if obj.route:
                        obj.route[0].setAttribute("edges", stopEdgeID)
                    elif obj.attr_from:
                        obj.attr_from = obj.to
                if options.color:
                    obj.setAttribute("color", options.color)

            outf.write(obj.toXML(' '*4))

    for objType, n in numSkipped.items():
        print("Warning: No stop added for %s %ss" % (n, objType))


def generateStationary(options, edge2parking, outf):
    paCapacity = {}
    if options.parking_areas:
        for pafile in options.parking_areas:
            for pa in sumolib.xml.parse(pafile, "parkingArea"):
                paCapacity[pa.id] = int(pa.getAttributeSecure("roadsideCapacity", 0))

    attrs = ""
    if options.duration:
        attrs += ' duration="%s"' % interpretDuration(options.duration)
    if options.until:
        attrs += ' until="%s"' % options.until

    for edge, pa in edge2parking.items():
        n = 0
        if options.rel_occupancy:
            n = int(paCapacity[pa] * options.rel_occupancy)
        elif options.abs_free:
            n = paCapacity[pa] - options.abs_free
        else:
            n = options.abs_occupancy
        color = ' color="%s"' % options.color if options.color else ''
        for i in range(n):
            id = "%s.%s" % (pa, i)
            outf.write('    <vehicle id="%s" depart="0" departPos="stop"%s>\n' % (id, color))
            outf.write('        <route edges="%s"/>\n' % edge)
            outf.write('        <stop parkingArea="%s"%s/>\n' % (pa, attrs))
            outf.write('    </vehicle>\n')


def main(options):
    random.seed(options.seed)

    edge2parking = {}
    if options.parking_areas:
        for pafile in options.parking_areas:
            for pa in sumolib.xml.parse(pafile, "parkingArea"):
                edge = '_'.join(pa.lane.split('_')[:-1])
                edge2parking[edge] = pa.id

    # with io.open(options.outfile, 'w', encoding="utf8") as outf:
    # with open(options.outfile, 'w', encoding="utf8") as outf:
    with open(options.outfile, 'w') as outf:
        sumolib.writeXMLHeader(outf, "$Id$", "routes", options=options)  # noqa
        if options.routefiles:
            loadRouteFiles(options, options.routefiles, edge2parking, outf)
        else:
            generateStationary(options, edge2parking, outf)
        outf.write('</routes>\n')
    outf.close()


if __name__ == "__main__":
    options = get_options()
    main(options)
