#!/usr/local/bin/python3.12
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.org/sumo
# Copyright (C) 2013-2019 German Aerospace Center (DLR) and others.
# This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v2.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v20.html
# SPDX-License-Identifier: EPL-2.0

# @file    traceExporter.py
# @author  Daniel Krajzewicz
# @author  Jakob Erdmann
# @author  Michael Behrisch
# @author  Evamarie Wiessner
# @date    2013-01-15
# @version $Id$


from __future__ import print_function
from __future__ import absolute_import
import os
import sys
import random
import datetime
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'tools'))
sys.path.append(os.path.join(os.environ.get(
    "SUMO_HOME", os.path.join(os.path.dirname(__file__), '..', '..')), 'tools'))

from sumolib.miscutils import getSocketStream  # noqa
import sumolib.net  # noqa
import sumolib.output.convert.phem as phem  # noqa
import sumolib.output.convert.omnet as omnet  # noqa
import sumolib.output.convert.shawn as shawn  # noqa
import sumolib.output.convert.ns2 as ns2  # noqa
import sumolib.output.convert.gpsdat as gpsdat  # noqa
import sumolib.output.convert.gpx as gpx  # noqa
import sumolib.output.convert.poi as poi  # noqa
import sumolib.output.convert.fcdfilter as fcdfilter  # noqa


class FCDTimeEntry:

    def __init__(self, t):
        self.time = t
        self.vehicle = []


def disturb_gps(x, y, deviation):
    if deviation == 0:
        return x, y
    x += random.gauss(0, deviation)
    y += random.gauss(0, deviation)
    return x, y


def _getOutputStream(name):
    if not name:
        return None
    return open(name, "w")


def _closeOutputStream(strm):
    if strm:
        strm.close()


def makeEntries(movables, chosen, options):
    if options.boundary:
        xmin, ymin, xmax, ymax = [float(e)
                                  for e in options.boundary.split(",")]
    result = []
    for v in movables:
        if v.id not in chosen:
            chosen[v.id] = random.random() < options.penetration
        if chosen[v.id]:
            v.x, v.y = disturb_gps(float(v.x), float(v.y), options.blur)
            if not v.z:
                v.z = 0
            if not options.boundary or (v.x >= xmin and v.x <= xmax and v.y >= ymin and v.y <= ymax):
                if v.lane:
                    v.edge = sumolib._laneID2edgeID(v.lane)
                result.append(v)
    return result


def procFCDStream(fcdstream, options):
    pt = -1  # "prior" time step
    lt = -1  # "last" time step
    lastExported = -1
    chosen = {}
    for q in fcdstream:
        pt = lt
        lt = float(q.time)
        if options.begin and options.begin > lt:
            continue  # do not export steps before a set begin
        if options.end and options.end <= lt:
            continue  # do not export steps after a set end
        if lastExported >= 0 and (options.delta and options.delta + lastExported > lt):
            continue  # do not export between delta-t, if set
        lastExported = lt
        e = FCDTimeEntry(lt)
        if q.vehicle:
            e.vehicle += makeEntries(q.vehicle, chosen, options)
        if options.persons and q.person:
            e.vehicle += makeEntries(q.person, chosen, options)
        yield e
    t = lt - pt + lt
    yield FCDTimeEntry(t)


def runMethod(inputFile, outputFile, writer, options, further={}):
    further["app"] = os.path.split(__file__)[1]
    further["orig-ids"] = options.orig_ids
    further["ignore-gaps"] = options.ignore_gaps
    if options.base >= 0:
        further["base-date"] = datetime.datetime.fromtimestamp(options.base)
    else:
        further["base-date"] = datetime.datetime.now().replace(hour=0,
                                                               minute=0, second=0, microsecond=0)
    o = _getOutputStream(outputFile)
    if inputFile.isdigit():
        inp = getSocketStream(int(inputFile))
    else:
        inp = inputFile
    fcdStream = sumolib.output.parse(inp, "timestep")
    ret = writer(procFCDStream(fcdStream, options), o, further)
    _closeOutputStream(o)
    return ret


def main(args=None):
    """The main function; parses options and converts..."""
    # ---------- build and read options ----------
    USAGE = """Usage %prog -i <FCDDATA> [options]
Converts the given fcd file (generated by sumo --fcd-output) into the selected
output format. Optionally the output can be sampled, filtered and distorted.
"""

    from optparse import OptionParser
    optParser = OptionParser(usage=USAGE)
    optParser.add_option("-i", "--fcd-input", dest="fcd", metavar="FILE",
                         help="Defines the FCD-output file to use as input " +
                              "(numeric value is interpreted as port to listen on)")
    optParser.add_option("-n", "--net-input", dest="net", metavar="FILE",
                         help="Defines the network file to use as input")
    optParser.add_option("-p", "--penetration", type="float", dest="penetration",
                         default=1., help="Defines the percentage (0-1) of vehicles to export")
    optParser.add_option("-b", "--begin", dest="begin",
                         type="float", help="Defines the first step to export")
    optParser.add_option("-e", "--end", dest="end",
                         type="float", help="Defines the first step not longer to export")
    optParser.add_option("-d", "--delta-t", dest="delta",
                         type="float", help="Defines the export step length")
    optParser.add_option("--gps-blur", dest="blur", default=0,
                         type="float", help="Defines the GPS blur")
    optParser.add_option(
        "--boundary", help="Defines the bounding box as 'xmin,ymin,xmax,ymax'")
    optParser.add_option("-s", "--seed", dest="seed", default=42,
                         type="float", help="Defines the randomizer seed")
    optParser.add_option(
        "--base-date", dest="base", default=-1, type="int", help="Defines the base date")
    optParser.add_option("--orig-ids", dest="orig_ids", default=False, action="store_true",
                         help="Write original vehicle IDs instead of a running index")
    optParser.add_option("--ignore-gaps", dest="ignore_gaps", default=False, action="store_true",
                         help="Ignore steps where a vehicle is not in the network")
    optParser.add_option("--persons", default=False, action="store_true",
                         help="Include person data")
    # PHEM
    optParser.add_option("--dri-output", dest="dri", metavar="FILE",
                         help="Defines the name of the PHEM .dri-file to generate")
    optParser.add_option("--str-output", dest="str", metavar="FILE",
                         help="Defines the name of the PHEM .str-file to generate")
    optParser.add_option("--fzp-output", dest="fzp", metavar="FILE",
                         help="Defines the name of the PHEM .fzp-file to generate")
    optParser.add_option("--flt-output", dest="flt", metavar="FILE",
                         help="Defines the name of the PHEM .flt-file to generate")
    # OMNET
    optParser.add_option("--omnet-output", dest="omnet", metavar="FILE",
                         help="Defines the name of the OMNET file to generate")
    # Shawn
    optParser.add_option("--shawn-output", dest="shawn", metavar="FILE",
                         help="Defines the name of the Shawn file to generate")
    # ns2
    optParser.add_option("--ns2activity-output", dest="ns2activity", metavar="FILE",
                         help="Defines the name of the ns2 file to generate")
    optParser.add_option("--ns2config-output", dest="ns2config", metavar="FILE",
                         help="Defines the name of the ns2 file to generate")
    optParser.add_option("--ns2mobility-output", dest="ns2mobility", metavar="FILE",
                         help="Defines the name of the ns2 file to generate")
    # GPSDAT
    optParser.add_option("--gpsdat-output", dest="gpsdat", metavar="FILE",
                         help="Defines the name of the gpsdat file to generate")

    # GPX
    optParser.add_option("--gpx-output", dest="gpx", metavar="FILE",
                         help="Defines the name of the gpx file to generate")
    # POI
    optParser.add_option("--poi-output", dest="poi", metavar="FILE",
                         help="Defines the name of the poi file to generate")
    # FCD
    optParser.add_option("--fcd-filter", dest="fcdfilter", metavar="FILE",
                         help="Defines the name of the filter definition file")
    optParser.add_option("--fcd-filter-comment", dest="fcdcomment",
                         help="Extra comments to include in fcd file")
    optParser.add_option("--fcd-filter-type", dest="fcdtype",
                         help="vehicle type to include in fcd file")
    # parse
    if len(args) == 1:
        sys.exit(USAGE.replace('%prog', os.path.basename(__file__)))
    options, remaining_args = optParser.parse_args(args=args)

    if options.seed:
        random.seed(options.seed)
    # ---------- process ----------
    net = None
    # ----- check needed values
    if options.delta and options.delta <= 0:
        print("delta-t must be a positive value.")
        return 1
    # phem
    if (options.dri or options.fzp or options.flt) and not options.fcd:
        print("A fcd-output from SUMO must be given using the --fcd-input.")
        return 1
    if (options.str or options.fzp or options.flt) and not options.net:
        print("A SUMO network must be given using the --net-input option.")
        return 1
    # omnet
    if options.omnet and not options.fcd:
        print("A fcd-output from SUMO must be given using the --fcd-input.")
        return 1
    # ----- check needed values

    # ----- OMNET
    if options.omnet:
        runMethod(options.fcd, options.omnet, omnet.fcd2omnet, options)
    # ----- OMNET

    # ----- Shawn
    if options.shawn:
        runMethod(options.fcd, options.shawn, shawn.fcd2shawn, options)
    # ----- Shawn

    # ----- GPSDAT
    if options.gpsdat:
        runMethod(options.fcd, options.gpsdat, gpsdat.fcd2gpsdat, options)
    # ----- GPSDAT

    # ----- GPX
    if options.gpx:
        runMethod(options.fcd, options.gpx, gpx.fcd2gpx, options)
    # ----- GPX

    # ----- POI
    if options.poi:
        runMethod(options.fcd, options.poi, poi.fcd2poi, options)
    # ----- POI

    # ----- FCD
    if options.fcdfilter:
        runMethod(options.fcd, None, fcdfilter.fcdfilter, options,
                  {"filter": options.fcdfilter, "comment": options.fcdcomment, "type": options.fcdtype})
    # ----- FCD

    # ----- ns2
    if options.ns2mobility or options.ns2config or options.ns2activity:
        vIDm, vehInfo, begin, end, area = runMethod(
            options.fcd, options.ns2mobility, ns2.fcd2ns2mobility, options)
    if options.ns2activity:
        o = _getOutputStream(options.ns2activity)
        ns2.writeNS2activity(o, vehInfo)
        _closeOutputStream(o)
    if options.ns2config:
        o = _getOutputStream(options.ns2config)
        ns2.writeNS2config(
            o, vehInfo, options.ns2activity, options.ns2mobility, begin, end, area)
        _closeOutputStream(o)
    # ----- ns2

    # ----- PHEM
    # .dri
    if options.dri:
        runMethod(options.fcd, options.dri, phem.fcd2dri, options)
    # .str (we need the net for other outputs, too)
    if options.str or options.fzp or options.flt:
        if not options.net:
            print("A SUMO network must be given using the --net-input option.")
            return 1
        if not net:
            net = sumolib.net.readNet(options.net)
        o = _getOutputStream(options.str)
        sIDm = phem.net2str(net, o)
        _closeOutputStream(o)
    # .fzp
    if options.flt or options.fzp:
        vIDm, vtIDm = runMethod(
            options.fcd, options.fzp, phem.fcd2fzp, options, {"phemStreetMap": sIDm})
    # .flt
    if options.flt:
        o = _getOutputStream(options.flt)
        phem.vehicleTypes2flt(o, vtIDm)
        _closeOutputStream(o)
    # ----- PHEM
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
