#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pkg_resources import resource_stream
import numpy as np
from .cysgp4 import PyDateTime, Satellite, PyObserver
from .cysgp4 import eci_to_geo, geo_to_eci, lookangles, _propagate_many_cysgp4


__all__ = ['get_example_tles', 'propagate_many_slow', 'propagate_many']


def get_example_tles():
    '''
    Return a text with example TLE entries for testing purposes.

    The example is the content of the file "science.txt", which was downloaded
    from `Celestrak <https://celestrak.com/>`_ on Nov 25 2019.

    Returns
    -------
    tle_text : str
        Text containing TLE line strings.


    Examples
    --------
    Use like this::

        >>> import cysgp4

        >>> tle_text = cysgp4.get_example_tles()
        >>> tle_text[:200]
        'AKEBONO (EXOS-D)        \\r\\n1 19822U 89016A   19321.49921565  .00016421  94291-6  28704-3 0  9992\\r\\n2 19822  75.0304 327.7460 1766579 276.4058  63.9734 12.00957719 13956\\r\\nHST                     \\r\\n1 2058'

        >>> tles = cysgp4.tle_tuples_from_text(tle_text)
        >>> tles  # doctest: +SKIP
        [('AKEBONO (EXOS-D)        ',
          '1 19822U 89016A   19321.49921565  .00016421  94291-6  28704-3 0  9992',
          '2 19822  75.0304 327.7460 1766579 276.4058  63.9734 12.00957719 13956'),
         ('HST                     ',
          '1 20580U 90037B   19321.38711875  .00000471  00000-0  17700-4 0  9991',
          '2 20580  28.4699 288.8102 0002495 321.7771 171.5855 15.09299865423838'),
        ...
         ('ZHANGZHENG-1 (CSES)     ',
          '1 43194U 18015C   19322.55091545  .00001006  00000-0  48427-4 0  9993',
          '2 43194  97.4135  85.6925 0016973 127.3937   1.4087 15.20935646 99453')]

    '''

    with resource_stream('cysgp4', 'tests/data/science.txt') as f:
        text = f.read()

    return text.decode('utf-8')


def propagate_many_slow(
        mjds, tles, observers=None,
        do_eci_pos=True, do_eci_vel=True,
        do_geo=True, do_topo=True,
        on_error='raise',
        ):
    '''
    This is a slow (non-parallelized, Python-looping) version of
    `~cysgp4.propagate_many` that is meant for testing and benchmarking
    only. It has the same interface.
    '''

    assert on_error in ['raise', 'coerce_to_nan']

    if observers is None:
        observers = PyObserver()

    pnum = 0
    out_dts = []
    if do_eci_pos:
        out_dts.append(np.dtype(('float64', 3)))
        pnum += 1
    if do_eci_vel:
        out_dts.append(np.dtype(('float64', 3)))
        pnum += 1
    if do_geo:
        out_dts.append(np.dtype(('float64', 3)))
        pnum += 1
    if do_topo:
        out_dts.append(np.dtype(('float64', 4)))
        pnum += 1

    it = np.nditer(
        [tles, observers, mjds] + [None] * pnum,
        flags=['external_loop', 'buffered', 'delay_bufalloc', 'refs_ok'],
        op_flags=[['readonly']] * 3 + [['readwrite', 'allocate']] * pnum,
        op_dtypes=['object', 'object', 'float64'] + out_dts
        )

    it.reset()
    for itup in it:

        tle = itup[0]
        obs = itup[1]
        mjd = itup[2]

        size = mjd.shape[0]
        for i in range(size):

            sat = Satellite(
                tle[i], obs[i], PyDateTime.from_mjd(mjd[i]),
                on_error=on_error
                )
            eci = sat.eci_pos()

            eci_pos_x, eci_pos_y, eci_pos_z = eci.loc
            eci_vel_x, eci_vel_y, eci_vel_z = eci.vel

            n = 3
            if do_eci_pos:
                itup[n][i][0] = eci_pos_x
                itup[n][i][1] = eci_pos_y
                itup[n][i][2] = eci_pos_z
                n += 1

            if do_eci_vel:
                itup[n][i][0] = eci_vel_x
                itup[n][i][1] = eci_vel_y
                itup[n][i][2] = eci_vel_z
                n += 1

            if do_geo:
                geo_pos = sat.geo_pos()
                itup[n][i][0] = geo_pos.lon
                itup[n][i][1] = geo_pos.lat
                itup[n][i][2] = geo_pos.alt
                n += 1

            if do_topo:
                topo_pos = sat.topo_pos()
                itup[n][i][0] = topo_pos.az
                itup[n][i][1] = topo_pos.el
                itup[n][i][2] = topo_pos.dist
                itup[n][i][3] = topo_pos.dist_rate
                n += 1

    result = {}

    n = 3
    if do_eci_pos:
        result['eci_pos'] = it.operands[n]
        n += 1

    if do_eci_vel:
        result['eci_vel'] = it.operands[n]
        n += 1

    if do_geo:
        result['geo'] = it.operands[n]
        n += 1

    if do_topo:
        result['topo'] = it.operands[n]
        n += 1

    return result


# def propagate_many_sgp4(
#         mjds, tles, observers=None,
#         do_eci_pos=True, do_eci_vel=True,
#         do_geo=True, do_topo=True,
#         do_obs_pos=False, do_sat_azel=False,
#         sat_frame='zxy',  # on_error='raise',
#         ):
#     '''
#     This is an `sgp4`_-based variant (non-parallelized, Python-looping) of
#     `~cysgp4.propagate_many` that is meant for testing and benchmarking
#     only. It has the same interface.

#     Note: `sgp4 <https://pypi.org/project/sgp4/>`_ Package by Brandon Rhodes
#     '''

#     try:
#         import sgp4
#     except ImportError as e:
#         import warnings
#         warnings.warn('''
#             Package sgp4 needed to use this function. Can install with
#             "pip install sgp4" or "conda install -c conda-forge sgp4".
#             ''')
#         raise e

#     # check whether sgp4 v1 or v2 are installed

#     try:
#         from sgp4 import api
#         version = 2
#     except ImportError:
#         version = 1

#     if version == 1:
#         from sgp4.earth_gravity import wgs72
#         from sgp4.io import twoline2rv
#     elif version == 2:
#         from sgp4.api import Satrec, SGP4_ERRORS

#     if observers is None:
#         observers = PyObserver()

#     out_dts = [np.dtype(('float64', 3))]

#     it = np.nditer(
#         [tles, observers, mjds] + [None] * 2,
#         flags=['external_loop', 'buffered', 'delay_bufalloc', 'refs_ok'],
#         op_flags=[['readonly']] * 3 + [['readwrite', 'allocate']] * 2,
#         op_dtypes=['object', 'object', 'float64'] + out_dts + out_dts
#         )

#     it.reset()
#     for itup in it:

#         tle = itup[0]
#         mjd = itup[2]

#         size = mjd.shape[0]

#         # TODO code path for api.accelerated == True

#         for i in range(size):

#             line1, line2 = tle[i].tle_strings()[1:]

#             if version == 1:
#                 dt_tup = PyDateTime.from_mjd(mjd[i]).get_datetime_tuple()
#                 sat = twoline2rv(line1, line2, wgs72)
#                 pos, vel = sat.propagate(
#                     *dt_tup[:-2], dt_tup[-2] + dt_tup[-1] / 1e6
#                     )
#             elif version == 2:
#                 sat = Satrec.twoline2rv(line1, line2)
#                 _f, _i = np.modf(mjd[i])
#                 err_code, pos, vel = sat.sgp4(_i + 2400000.5, _f)

#                 if err_code:
#                     raise ValueError(
#                         'Satellite propagation error', err_code,
#                         '({})'.format(SGP4_ERRORS[err_code])
#                         )

#             eci_pos_x, eci_pos_y, eci_pos_z = pos
#             eci_vel_x, eci_vel_y, eci_vel_z = vel

#             itup[3][i][0] = eci_pos_x
#             itup[3][i][1] = eci_pos_y
#             itup[3][i][2] = eci_pos_z

#             itup[4][i][0] = eci_vel_x
#             itup[4][i][1] = eci_vel_y
#             itup[4][i][2] = eci_vel_z

#     eci_pos = it.operands[3]
#     eci_vel = it.operands[4]
#     eci_pos_x, eci_pos_y, eci_pos_z = (eci_pos[..., i] for i in range(3))
#     eci_vel_x, eci_vel_y, eci_vel_z = (eci_vel[..., i] for i in range(3))

#     # why is this not working???
#     # obs_geo_lon, obs_geo_lat, obs_geo_alt = np.vectorize(
#     #     lambda o: np.array([o.loc.lon, o.loc.lat, o.loc.alt]),
#     #     otypes=[np.float64, np.float64, np.float64]
#     #     )(observers)

#     obs_geo_lon = np.empty(observers.shape, dtype=np.float64)
#     obs_geo_lon.flat[:] = [o.loc.lon for o in observers.flat]
#     obs_geo_lat = np.empty(observers.shape, dtype=np.float64)
#     obs_geo_lat.flat[:] = [o.loc.lat for o in observers.flat]
#     obs_geo_alt = np.empty(observers.shape, dtype=np.float64)
#     obs_geo_alt.flat[:] = [o.loc.alt for o in observers.flat]

#     (
#         tles,
#         obs_geo_lon, obs_geo_lat, obs_geo_alt,
#         mjds,
#         eci_pos_x, eci_pos_y, eci_pos_z,
#         eci_vel_x, eci_vel_y, eci_vel_z,
#         ) = np.broadcast_arrays(
#             tles,
#             obs_geo_lon, obs_geo_lat, obs_geo_alt,
#             mjds,
#             eci_pos_x, eci_pos_y, eci_pos_z,
#             eci_vel_x, eci_vel_y, eci_vel_z,
#             )

#     result = {}

#     if do_eci_pos:
#         result['eci_pos'] = np.stack(
#             [eci_pos_x, eci_pos_y, eci_pos_z], axis=-1
#             )

#     if do_eci_vel:
#         result['eci_vel'] = np.stack(
#             [eci_vel_x, eci_vel_y, eci_vel_z], axis=-1
#             )

#     if do_geo:
#         geo_lon, geo_lat, geo_alt = eci_to_geo(
#             eci_pos_x, eci_pos_y, eci_pos_z, mjds
#             )
#         result['geo'] = np.stack([geo_lon, geo_lat, geo_alt], axis=-1)

#     if do_topo or do_sat_azel:
#         obs_az, obs_el, sat_az, sat_el, dist, dist_rate = lookangles(
#             eci_pos_x, eci_pos_y, eci_pos_z,
#             eci_vel_x, eci_vel_y, eci_vel_z,
#             mjds, observers, sat_frame
#             )
#         topo = np.stack([obs_az, obs_el, dist, dist_rate], axis=-1)
#         sat_azel = np.stack([sat_az, sat_el, dist], axis=-1)

#     if do_topo:
#         result['topo'] = topo

#     if do_sat_azel:
#         result['sat_azel'] = sat_azel

#     if do_obs_pos:
#         obs_x, obs_y, obs_z = geo_to_eci(
#             obs_geo_lon, obs_geo_lat, obs_geo_alt, mjds
#             )
#         result['obs_pos'] = np.stack([obs_x, obs_y, obs_z], axis=-1)

#     return result


def _sat_eci_sgp4_nonaccelerated(tles, observers, mjds):

    from sgp4.api import Satrec, SGP4_ERRORS

    out_dts = [np.dtype(('float64', 3))]

    it = np.nditer(
        [tles, observers, mjds] + [None] * 2,
        flags=['external_loop', 'buffered', 'delay_bufalloc', 'refs_ok'],
        op_flags=[['readonly']] * 3 + [['readwrite', 'allocate']] * 2,
        op_dtypes=['object', 'object', 'float64'] + out_dts + out_dts
        )

    it.reset()
    for itup in it:

        tle = itup[0]
        mjd = itup[2]

        size = mjd.shape[0]

        # TODO code path for api.accelerated == True

        for i in range(size):

            line1, line2 = tle[i].tle_strings()[1:]
            sat = Satrec.twoline2rv(line1, line2)
            _f, _i = np.modf(mjd[i])
            err_code, pos, vel = sat.sgp4(_i + 2400000.5, _f)

            if err_code:
                raise ValueError(
                    'Satellite propagation error', err_code,
                    '({})'.format(SGP4_ERRORS[err_code])
                    )

            eci_pos_x, eci_pos_y, eci_pos_z = pos
            eci_vel_x, eci_vel_y, eci_vel_z = vel

            itup[3][i][0] = eci_pos_x
            itup[3][i][1] = eci_pos_y
            itup[3][i][2] = eci_pos_z

            itup[4][i][0] = eci_vel_x
            itup[4][i][1] = eci_vel_y
            itup[4][i][2] = eci_vel_z

    eci_pos = it.operands[3]
    eci_vel = it.operands[4]

    return eci_pos, eci_vel


def _sat_eci_sgp4_accelerated(tles, observers, mjds):
    '''
    Note, on a single core, this version is not really faster than
    the non-accelerated version. Probably the overhead of the python
    iteration can mostly be neglected.
    '''

    from sgp4.api import Satrec, SGP4_ERRORS

    unique_tles = np.unique(tles)
    tles, observers, mjds = np.broadcast_arrays(tles, observers, mjds)

    eci_pos = np.empty(tles.shape + (3,), dtype=np.float64)
    eci_vel = np.empty(tles.shape + (3,), dtype=np.float64)

    for u_tle in unique_tles:

        # doesn't work, as array doesn't know tle_strings method
        # mask = u_tle == tles
        mask = np.array(
            [u_tle == t for t in tles.flat], dtype=np.bool
            ).reshape(tles.shape)

        mjd = mjds[mask]

        line1, line2 = u_tle.tle_strings()[1:]

        sat = Satrec.twoline2rv(line1, line2)
        _f, _i = np.modf(mjd)
        err_code, pos, vel = sat.sgp4_array(_i + 2400000.5, _f)

        if np.any(err_code):
            raise ValueError(
                'Satellite propagation error', err_code,
                '({})'.format(SGP4_ERRORS[err_code])
                )

        eci_pos[mask] = pos
        eci_vel[mask] = vel

    return eci_pos, eci_vel


def _propagate_many_sgp4(
        mjds, tles, observers=None,
        do_eci_pos=True, do_eci_vel=True,
        do_geo=True, do_topo=True,
        do_obs_pos=False, do_sat_azel=False,
        sat_frame='zxy',  # on_error='raise',
        ):
    '''
    This is an `sgp4`_-based variant (Python-looping) of
    `~cysgp4.propagate_many` that is meant for testing and benchmarking
    only. It has the same interface.

    Note: `sgp4 <https://pypi.org/project/sgp4/>`_ Package by Brandon Rhodes
    '''

    try:
        from sgp4 import api
    except ImportError as e:
        import warnings
        warnings.warn('''
            Package sgp4 2.x needed to use this function. Can install with
            "pip install sgp4" or "conda install -c conda-forge sgp4".
            ''')
        raise e

    if observers is None:
        observers = PyObserver()

    if api.accelerated:
        eci_pos, eci_vel = _sat_eci_sgp4_accelerated(tles, observers, mjds)
    else:
        eci_pos, eci_vel = _sat_eci_sgp4_nonaccelerated(tles, observers, mjds)

    eci_pos_x, eci_pos_y, eci_pos_z = (eci_pos[..., i] for i in range(3))
    eci_vel_x, eci_vel_y, eci_vel_z = (eci_vel[..., i] for i in range(3))

    # why is this not working???
    # obs_geo_lon, obs_geo_lat, obs_geo_alt = np.vectorize(
    #     lambda o: np.array([o.loc.lon, o.loc.lat, o.loc.alt]),
    #     otypes=[np.float64, np.float64, np.float64]
    #     )(observers)

    obs_geo_lon = np.empty(observers.shape, dtype=np.float64)
    obs_geo_lon.flat[:] = [o.loc.lon for o in observers.flat]
    obs_geo_lat = np.empty(observers.shape, dtype=np.float64)
    obs_geo_lat.flat[:] = [o.loc.lat for o in observers.flat]
    obs_geo_alt = np.empty(observers.shape, dtype=np.float64)
    obs_geo_alt.flat[:] = [o.loc.alt for o in observers.flat]

    (
        tles,
        obs_geo_lon, obs_geo_lat, obs_geo_alt,
        mjds,
        eci_pos_x, eci_pos_y, eci_pos_z,
        eci_vel_x, eci_vel_y, eci_vel_z,
        ) = np.broadcast_arrays(
            tles,
            obs_geo_lon, obs_geo_lat, obs_geo_alt,
            mjds,
            eci_pos_x, eci_pos_y, eci_pos_z,
            eci_vel_x, eci_vel_y, eci_vel_z,
            )

    result = {}

    if do_eci_pos:
        result['eci_pos'] = np.stack(
            [eci_pos_x, eci_pos_y, eci_pos_z], axis=-1
            )

    if do_eci_vel:
        result['eci_vel'] = np.stack(
            [eci_vel_x, eci_vel_y, eci_vel_z], axis=-1
            )

    if do_geo:
        geo_lon, geo_lat, geo_alt = eci_to_geo(
            eci_pos_x, eci_pos_y, eci_pos_z, mjds
            )
        result['geo'] = np.stack([geo_lon, geo_lat, geo_alt], axis=-1)

    if do_topo or do_sat_azel:
        obs_az, obs_el, sat_az, sat_el, dist, dist_rate = lookangles(
            eci_pos_x, eci_pos_y, eci_pos_z,
            eci_vel_x, eci_vel_y, eci_vel_z,
            mjds, observers, sat_frame
            )
        topo = np.stack([obs_az, obs_el, dist, dist_rate], axis=-1)
        sat_azel = np.stack([sat_az, sat_el, dist], axis=-1)

    if do_topo:
        result['topo'] = topo

    if do_sat_azel:
        result['sat_azel'] = sat_azel

    if do_obs_pos:
        obs_x, obs_y, obs_z = geo_to_eci(
            obs_geo_lon, obs_geo_lat, obs_geo_alt, mjds
            )
        result['obs_pos'] = np.stack([obs_x, obs_y, obs_z], axis=-1)

    return result


def propagate_many(
        mjds, tles, observers=None,
        do_eci_pos=True, do_eci_vel=True,
        do_geo=True, do_topo=True,
        do_obs_pos=False, do_sat_azel=False,
        sat_frame='zxy', on_error='raise',
        method='dwarner'
        ):
    '''
    Calculate positions of many satellites at a various times at once.

    This is an array interface to the sgp4 calculations, which allows to
    perform calculations for different satellite TLEs, observers and times
    in a parallelized manner. `~numpy` broadcasting rules apply.

    With the Boolean parameters, `do_eci_pos`, `do_eci_vel`, `do_geo`,
    `do_topo`, `do_obs_pos`, and `do_sat_azel` the user can decide, which
    position frames are returned in the output dictionary.

    Satellite are defined via TLEs (see `~cysgp4.PyTle`). The
    `~.cysgp4.propagate_many` function works with a single (scalar) PyTle
    object or a list/array of them. The same is true for the `mjds` and
    optional `observers` parameters, which must be a scalar (a double and
    `~cysgp4.PyObserver` instance, respectively) or a list/array of them.

    As in most use cases, a large number of positions is probably queried,
    the returned values do not use the `~cysgp4.PyEci` or
    `~cysgp4.PyCoordTopocentric` classes, but pack everything in arrays.
    This is not only faster, but makes it easier to further process the
    results.

    For parallelization, `OpenMP <https://www.openmp.org/>`_ is utilized.
    To change the number of CPU cores that are used, one can either set
    the environment variable `OMP_NUM_THREADS` or use
    `~cysgp4.set_num_threads`.

    Parameters
    ----------
    mjds : `~numpy.ndarray`, `~list`, or scalar of float
        Modified Julian Date.
    tles : `~numpy.ndarray`, `~list`, or scalar of `~cysgp4.PyTle`
        TLE instance of the satellite of interest.
    observers : `~numpy.ndarray`, `~list`, or scalar of `~cysgp4.PyObserver` or None (default: None)
        Observer instance. If `None` then the observer location is set to
        (0 deg, 0 deg, 0 km).
    do_eci_pos : Boolean, optional (default: True)
        Whether to include ECI cartesian positions in the result.
    do_eci_vel : Boolean, optional (default: True)
        Whether to include ECI cartesian velocity in the result.
    do_geo : Boolean, optional (default: True)
        Whether to include geographic/geodetic positions in the result.
    do_topo : Boolean, optional (default: True)
        Whether to include topocentric positions in the result.
    do_obs_pos : Boolean, optional (default: True)
        Whether to include the observer ECI position in the results.
    do_sat_azel : Boolean, optional (default: True)
        Whether to include the observer position as seen by the satellite
        (distance/azimuth/elevation) in the results.
    sat_frame : 'zxy' or 'xyz', optional (default: 'zxy')
        How the moving satellite frame is defined. Two options are
        implemented, 'zxy' and 'xyz'.  If 'zxy' is chosen, the moving
        satellite frame is constructed such that the `z` axis is aligned with
        the satellite motion vector. The `y` axis is constructed as a vector
        that is perpendicular to the plane defined by the motion vector and
        the ECI zero point (aka Earth center). The resulting `x` axis, which
        is orthogonal to the `y` and `z` axes, is then approximately pointing
        to the nadir. Alternatively, if the frame is set as `xyz`, the `x`
        axes is the motion vector, `y` has the same meaning and `z` is
        approximately pointing towards the nadir.
    on_error : str, optional (either 'raise' or 'coerce_to_nan', default: 'raise')
        If the underlying SGP C++ library throws an error (which often
        happens if one works with times that are strongly deviating from
        the TLE epoch), a Python RuntimeError is usually thrown. For batch
        processing, this is not always desirable. If
        `on_error = 'coerce_to_nan'` then C++ errors will be suppressed and
        the resulting position vectors will be converted to NaN-values
        instead.

        Note: this option is only relevant for `method='dwarner'`!
    method : str, optional (either 'dwarner' or 'vallado')
        This option can be used to change from the default SGP C++ library
        (by Daniel Warner) to the Vallado model. For the latter, the
        external `python-sgp4` library is used. 'vallado' may be slower
        due to the additional boiler-plate code and differences in the
        model itself and implementation details.

    Returns
    -------
    result : dict
        Resulting positions for each requested frame:

        - `eci_pos` : `~numpy.ndarray` of float

          Satellites ECI cartesian positions. Last dimension has length 3,
          one for each of `x`, `y`, and `z`.

        - `eci_vel` : `~numpy.ndarray` of float

          Satellites ECI cartesian velicities. Last dimension has length 3,
          one for each of `v_x`, `v_y`, and `v_z`.

        - `geo` : `~numpy.ndarray` of float

          Satellites Geodetic positions. Last dimension has length 3, one
          for each of `lon`, `lat`, and `alt`.

        - `topo` : `~numpy.ndarray` of float

          Satellites Topocentric positions. Last dimension has length 4,
          one for each of `az`, `el`, `dist`, and `dist_rate`.

        - `obs_pos` : `~numpy.ndarray` of float

          Observer positions in ECI frame (Cartesian). Last dimension has
          length 3, one for each of `x`, `y`, and `z`.

        - `sat_azel` : `~numpy.ndarray` of float

          Observer positions in the (moving) satellite frame either given as
          azimuth (aka across track), elevation (aka along track), and
          distance - if `sat_frame` is 'zxy', or given as 'theta', 'phi', and
          distance if `sat_frame` is 'xyz' (with ISO definition of the angles
          theta and phi); see also `sat_frame` parameter description. Last
          dimension has length 3, one for each of, (`az`, `el`, `dist`) or
          (`theta', `phi`, `dist`), respectively.

        In all cases the first dimensions are determined by the
        (broadcasted) shape of the inputs `mjd`, `tles`, and `observers`.

    Examples
    --------
    The following demonstrates how to use the `~cysgp4.propagate_many`
    function::

        >>> import numpy as np
        >>> from cysgp4 import PyTle, PyObserver, propagate_many
        >>> from cysgp4 import get_example_tles, tles_from_text

        >>> tle_text = get_example_tles()
        >>> tles = np.array(
        ...     tles_from_text(tle_text)
        ...     )[np.newaxis, np.newaxis, :20]  # use first 20 TLEs
        >>> observers = np.array([
        ...     PyObserver(6.88375, 50.525, 0.366),
        ...     PyObserver(16.88375, 50.525, 0.366),
        ...     ])[np.newaxis, :, np.newaxis]
        >>> mjds = np.linspace(
        ...     58805.5, 58806.5, 1000  # 1000 time steps
        ...     )[:, np.newaxis, np.newaxis]

        >>> result = propagate_many(mjds, tles, observers)
        >>> print(sorted(result.keys()))
        ['eci_pos', 'eci_vel', 'geo', 'topo']

        >>> # shapes are as follows
        >>> print(np.broadcast(mjds, tles, observers).shape)
        (1000, 2, 20)
        >>> print(result['eci_pos'].shape, result['topo'].shape)
        (1000, 2, 20, 3) (1000, 2, 20, 4)

        >>> result = propagate_many(
        ...     mjds, tles, observers,
        ...     do_eci_pos=False, do_eci_vel=False, do_geo=False, do_topo=True
        ...     )
        >>> print(sorted(result.keys()))
        ['topo']

    '''

    if method not in ['dwarner', 'vallado']:
        raise ValueError('"method" most be one of ["dwarner", "vallado"]')

    if on_error not in ['raise', 'coerce_to_nan']:
        raise ValueError(
            '"on_error" most be one of ["raise", "coerce_to_nan"]'
            )

    if sat_frame not in ['zxy', 'xyz']:
        raise ValueError('"sat_frame" most be one of ["zxy", "xyz"]')

    if method == 'dwarner':

        return _propagate_many_cysgp4(
            mjds, tles, observers=observers,
            do_eci_pos=do_eci_pos, do_eci_vel=do_eci_vel,
            do_geo=do_geo, do_topo=do_topo,
            do_obs_pos=do_obs_pos, do_sat_azel=do_sat_azel,
            sat_frame=sat_frame, on_error=on_error,
            )

    elif method == 'vallado':

        return _propagate_many_sgp4(
            mjds, tles, observers=observers,
            do_eci_pos=do_eci_pos, do_eci_vel=do_eci_vel,
            do_geo=do_geo, do_topo=do_topo,
            do_obs_pos=do_obs_pos, do_sat_azel=do_sat_azel,
            sat_frame=sat_frame,
            )
