# -*- coding: utf-8 -*-
"""
CNV file format support for ObsPy

:copyright:
    The ObsPy Development Team (devs@obspy.org)
:license:
    GNU Lesser General Public License, Version 3
    (https://www.gnu.org/copyleft/lesser.html)
"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
from future.builtins import *  # NOQA @UnusedWildImport

from bisect import bisect_right
import sys
import warnings


from obspy.core.util.deprecation_helpers import \
    DynamicAttributeImportRerouteModule


def _write_cnv(catalog, filename, phase_mapping=None, ifx_list=None,
               weight_mapping=None, default_weight=0):
    """
    Write a :class:`~obspy.core.event.Catalog` object to CNV event summary
    format (used as event/pick input by VELEST program).

    .. warning::
        This function should NOT be called directly, it registers via the
        the :meth:`~obspy.core.event.Catalog.write` method of an
        ObsPy :class:`~obspy.core.event.Catalog` object, call this instead.

    :type catalog: :class:`~obspy.core.event.Catalog`
    :param catalog: Input catalog for CNV output..
    :type filename: str or file
    :param filename: Filename to write or open file-like object.
    :type phase_mapping: dict
    :param phase_mapping: Mapping of phase hints to "P" or "S". CNV format only
        uses a single letter phase code (either "P" or "S"). If not specified
        the following default mapping is used: 'p', 'P', 'Pg', 'Pn', 'Pm' will
        be mapped to "P" and 's', 'S', 'Sg', 'Sn', 'Sm' will be mapped to "S".
    :type ifx_list: list of :class:`~obspy.core.event.ResourceIdentifier`
    :param ifx_list: List of events for which the 'IFX' flag should be set
        (used in VELEST to fix the y coordinate of the hypocenter).
    :type weight_mapping: list of float
    :param weight_mapping: Mapping of pick uncertainties to integer weights.
        (Sorted) list of floats of boundary uncertainties. If uncertainty of
        pick is lower than the first entry of the list than a weight of 0 is
        assigned. If it is larger than the first entry, but smaller than the
        second entry a weight of 1 is assigned, and so on. The list of
        uncertainty boundaries should not contain more than 9 entries because
        the integer weight is restricted to a single digit. If not specified
        all picks will be output with weight `default_weight`.
    :type default_weight: int
    :param default_weight: Default weight to use when pick has no timing
        uncertainty and thus can not be mapped using `weight_mapping`
        parameter. Default weight should not be larger than 9, as the weight is
        represented as a single digit.
    """
    # Check phase mapping or use default one
    if phase_mapping is None:
        phase_mapping = {'p': "P", 'P': "P", 'Pg': "P", 'Pn': "P", 'Pm': "P",
                         's': "S", 'S': "S", 'Sg': "S", 'Sn': "S", 'Sm': "S"}
    else:
        values = set(phase_mapping.values())
        if values.update(("P", "S")) != set(("P", "S")):
            msg = ("Values of phase mapping should only be 'P' or 'S'")
            raise ValueError(msg)
    if ifx_list is None:
        ifx_list = []
    if weight_mapping is None:
        weight_mapping = []
    else:
        if list(weight_mapping) != sorted(weight_mapping):
            msg = ("List of floats in weight mapping must be sorted in "
                   "ascending order.")
            raise ValueError(msg)

    out = []
    for event in catalog:
        o = event.preferred_origin() or event.origins[0]
        m = event.preferred_magnitude() or event.magnitudes[0]

        out_ = "%s %5.2f %7.4f%1s %8.4f%1s%7.2f%7.2f%2i\n"
        cns = o.latitude >= 0 and "N" or "S"
        cew = o.longitude >= 0 and "E" or "W"
        if event.resource_id in ifx_list:
            ifx = 1
        else:
            ifx = 0
        out_ = out_ % (o.time.strftime("%y%m%d %H%M"),
                       o.time.second + o.time.microsecond / 1e6,
                       abs(o.latitude), cns, abs(o.longitude), cew,
                       o.depth / 1e3, m.mag, ifx)
        # assemble phase info
        picks = []
        for p in event.picks:
            # map uncertainty to integer weight
            if p.time_errors.upper_uncertainty is not None and \
                    p.time_errors.lower_uncertainty is not None:
                uncertainty = p.time_errors.upper_uncertainty + \
                    p.time_errors.lower_uncertainty
            else:
                uncertainty = p.time_errors.uncertainty
            if uncertainty is None:
                msg = ("No pick time uncertainty, pick will be mapped to "
                       "default integer weight (%s).") % default_weight
                warnings.warn(msg)
                weight = default_weight
            else:
                weight = bisect_right(weight_mapping, uncertainty)
            if weight > 9:
                msg = ("Integer weight for pick is greater than 9. "
                       "This is not compatible with the single-character "
                       "field for pick weight in CNV format."
                       "Using 9 as integer weight.")
                warnings.warn(msg)
                weight = 9
            # map phase hint
            phase = phase_mapping.get(p.phase_hint, None)
            if phase is None:
                msg = "Skipping pick (%s) with unmapped phase hint: %s"
                msg = msg % (p.waveform_id.get_seed_string(), p.phase_hint)
                warnings.warn(msg)
                continue
            station = p.waveform_id.station_code
            if len(station) > 4:
                msg = ("Station code with more than 4 characters detected. "
                       "Only the first 4 characters will be used in output.")
                warnings.warn(msg)
                station = station[:4]
            dt = "%6.2f" % (p.time - o.time)
            if len(dt) != 6:
                msg = ("Problem with pick (%s): Calculated travel time '%s' "
                       "does not fit in the '%%6.2f' fixed format field. "
                       "Skipping this pick.")
                msg = msg % (p.waveform_id.get_seed_string(), dt)
                warnings.warn(msg)
                continue
            picks.append("".join([station.ljust(4), phase, str(weight), dt]))
        while len(picks) > 6:
            next_picks, picks = picks[:6], picks[6:]
            out_ += "".join(next_picks) + "\n"
        if picks:
            out_ += "".join(picks) + "\n"
        out.append(out_)

    if out:
        out = "\n".join(out + [""])
    else:
        msg = "No event/pick information, writing empty CNV file."
        warnings.warn(msg)

    # Open filehandler or use an existing file like object.
    if not hasattr(filename, "write"):
        file_opened = True
        fh = open(filename, "wb")
    else:
        file_opened = False
        fh = filename

    fh.write(out.encode())

    # Close if a file has been opened by this function.
    if file_opened is True:
        fh.close()


# Remove once 0.11 has been released.
sys.modules[__name__] = DynamicAttributeImportRerouteModule(
    name=__name__, doc=__doc__, locs=locals(),
    original_module=sys.modules[__name__],
    import_map={},
    function_map={
        'write_CNV': 'obspy.io.cnv.core._write_cnv'})


if __name__ == '__main__':
    import doctest
    doctest.testmod(exclude_empty=True)
