
from collections import OrderedDict
from typing import Union
import numbers
from enum import Enum


class PointLikePlotTypes(Enum):
    POINTS = 1
    ATTITUDES = 2
    FOCAL_MECHANISMS = 3


class FocalMechanismView(Enum):
    """
    MAP: the beachball is drawn as the original map-view glyph (strike/dip/rake as given),
    not rotated into the section plane.
    SECTION: the focal mechanism is rotated so that it is drawn as it would appear viewed
    horizontally from the section, perpendicular to the profile direction, i.e., a true
    cross-section projection of the focal sphere.
    """
    MAP = 1
    SECTION = 2

breakline_width = 0.6
breakline_color = "darkseagreen"


# General figure

FIG_WIDTH_INCHES_DEFAULT = 8.5
FIG_HEIGHT_INCHES_DEFAULT = 5.0
FIG_Z_PADDING_DEFAULT = 0.2

# Grids

GRID_COLORMAP_DEFAULT: str = "gist_earth"
GRID_USE_HILLSHADE_DEFAULT: bool = True
GRID_HS_VERT_EXAGG_DEFAULT: numbers.Real = 1.0
GRID_HS_BLEND_MODE_DEFAULT: str = 'overlay'  # one of 'hillshade', 'hsv', 'overlay', 'soft'
GRID_HS_LIGHT_SOURCE_AZIM_DEFAULT: numbers.Real = 315.0
GRID_HS_LIGHT_SOURCE_DEGR_DEFAULT: numbers.Real = 45.0
GRID_PLOT_COLORBAR_DEFAULT: bool = True

# Profiles

TOPOPROF_ASPECT_DEFAULT = 1
TOPOPROF_MAX_PTS_NUM_DEFAULT = 1e4
TOPOPROF_LINE_COLOR_DEFAULT = 'grey'
TOPOPROF_SUPERPOSED_CHOICE_DEFAULT = False

# Vertical axis labelling.
#
# The units and the direction are options rather than defaults, because they are
# not a matter of taste: a normal geological profile carries its topography above
# sea level, where labelling elevations as positive depths would be simply wrong.
# They are meant for sections whose subject lies below the surface — hypocentres,
# say — where depth in km is the convention.
ZAXIS_UNITS_DEFAULT = "m"
ZAXIS_AS_DEPTH_DEFAULT = False

# Unit the along-profile distance is labelled in. Kept separate from the vertical
# one, since a short profile of deep hypocentres reads better with the distance in
# metres and the depth in km, but the two are normally set to match.
SAXIS_UNITS_DEFAULT = "m"

# Whether to name the axes and their units on the figure. On by default: unlike
# the unit and direction choices, naming an axis is never wrong, and without it a
# reader cannot tell metres from km, nor a depth from an elevation.
AXIS_LABELS_DEFAULT = True

# How far, in points, an axis name sits from its box: past the tick labels, which
# already occupy that side. Room for the widest label the vertical axis is likely
# to carry, e.g. a five-character depth, at the tick font size.
AXIS_LABEL_PAD_POINTS = 30
AXIS_LABEL_FONT_SIZE = 9

# axis label unit -> metres per unit, for either axis
axis_units = OrderedDict(
    [
        ("m", 1.0),
        ("km", 1000.0),
    ]
)

# Upper bound on the number of vertical tick labels per profile. Stacked profiles
# get one axis each, so a count that reads well on a single plot becomes a wall of
# numbers over nine of them.
ZAXIS_MAX_TICKS_DEFAULT = 5

# Inset, in points, of a profile's own number from the top left corner of its box,
# the number sitting inside the box: to its left is the column of elevation tick
# labels, whose width varies with the values shown, and above it the box of the
# profile before, stacked flush against this one.
PROFILE_LABEL_INSET_POINTS = 5

# Font size of a profile's own number, and the opacity of the patch it is written
# on, which keeps it readable where the topography reaches the corner.
PROFILE_LABEL_FONT_SIZE = 8
PROFILE_LABEL_BACKGROUND_ALPHA = 0.7

# Relative closeness to the top of the view at which a vertical tick counts as
# sitting on it, and so is left unlabelled.
ZAXIS_TOP_LABEL_TOLERANCE = 1.0e-9

# General

# lines
LN_MARKER_STYLE_DEFAULT: str = '-'
LN_WIDTH_STYLE_DEFAULT: numbers.Real = 0.95

# labels
LBLS_PLOT_DEFAULT: bool = True
LBLS_CONTENT_DEFAULT: str = 'key'  # values: 'key', 'index', 'orientation', 'index-orientation',


# Points/Attitudes

PTATT_COLOR_DEFAULT = "red"
PTATT_MARKER_DEFAULT = 'o'
PTATT_MARKER_SIZE_DEFAULT = 6
PTATT_ALPHA_SIZE_DEFAULT = 1.0
PTATT_PLOT_TYPE_DEFAULT = PointLikePlotTypes.POINTS
PTATT_LABELS_ORIENTIONS_DEFAULT = True
PTATT_LABELS_IDS_DEFAULT = False

# Focal mechanisms (obspy beachballs)

FMEC_WIDTH_DEFAULT = 25  # in points, i.e. screen size, not data units
FMEC_FACECOLOR_DEFAULT = "black"
FMEC_BGCOLOR_DEFAULT = "white"
FMEC_EDGECOLOR_DEFAULT = "black"
# outline width, in points. obspy draws beachballs with a 2 point outline, sized
# for the large stand-alone glyph it has in mind: on a beachball a few tens of
# points wide, plotted a dozen at a time on a section, that much ink turns the
# quadrant boundaries into a blot and hides the mechanism it is meant to show
FMEC_LINEWIDTH_DEFAULT = 0.5
FMEC_ALPHA_DEFAULT = 1.0
FMEC_LABELS_DEFAULT = False
FMEC_VIEW_DEFAULT = FocalMechanismView.MAP
FMEC_LABEL_FONT_SIZE = 7
# gap left between the beachball edge and its label, in points, i.e. the same
# screen units the beachball width is given in
FMEC_LABEL_GAP_POINTS = 2

# Lines intersections

LNINT_COLOR_DEFAULT = "red"
LNINT_SIZE_DEFAULT = 3
LNINT_ALPHA_DEFAULT = 0.5
LNINT_LABELS_DEFAULT = True
LNINT_ADDITIONAL_COLORS = [
    "red",
    "green",
    "blue",
    "yellow",
    "hotpink",
    "darkseagreen",
    "darkgoldenrod",
    "darkviolet",
    "powderblue",
    "yellowgreen",
    "palevioletred",
    "seagreen",
    "darkturquoise",
    "beige",
    "darkkhaki",
    "magenta",
    "cyan",
    "chartreuse"
]

# Polygons intersections

PLINT_LINE_WIDTH_DEFAULT = 2
PLINT_LABELS_DEFAULT = True
PLINT_LEGEND_DEFAULT = True


max_parallel_profiles_number = 20


class FigureParams:

    def __init__(
            self,
            width: numbers.Real = FIG_WIDTH_INCHES_DEFAULT,
            height: numbers.Real = FIG_HEIGHT_INCHES_DEFAULT,
    ):
        self.width = width
        self.height = height


class AxisPlotParams:

    def __init__(self,
                 z_min: Union[None, numbers.Real] = -1000,
                 z_max: Union[None, numbers.Real] = 1000,
                 vertical_exaggeration: numbers.Real = TOPOPROF_ASPECT_DEFAULT,
                 grid=True,  #
                 grid_color='tan',  #
                 grid_linestyle='--',  #
                 grid_linewidth=0.2,  #
                 breaklines=True,  #
                 breaklines_color=breakline_color,  #
                 breaklines_width=breakline_width,  #
                 breaklines_style='dotted',  #
                 z_axis_units: str = ZAXIS_UNITS_DEFAULT,
                 z_axis_as_depth: bool = ZAXIS_AS_DEPTH_DEFAULT,
                 z_axis_max_ticks: numbers.Integral = ZAXIS_MAX_TICKS_DEFAULT,
                 s_axis_units: str = SAXIS_UNITS_DEFAULT,
                 show_axis_labels: bool = AXIS_LABELS_DEFAULT,
                 ):

        self.z_min = z_min
        self.z_max = z_max
        self.vertical_exaggeration = vertical_exaggeration
        self.grid = grid
        self.grid_color = grid_color
        self.grid_linestyle = grid_linestyle
        self.grid_linewidth = grid_linewidth
        self.breaklines = breaklines
        self.breaklines_color = breaklines_color
        self.breaklines_width = breaklines_width
        self.breaklines_style = breaklines_style
        # the vertical values stay in metres, elevation-positive, throughout: these
        # only change how the axis is labelled
        self.z_axis_units = z_axis_units
        self.z_axis_as_depth = z_axis_as_depth
        self.z_axis_max_ticks = z_axis_max_ticks
        self.s_axis_units = s_axis_units
        self.show_axis_labels = show_axis_labels

    def __repr__(self):

        return f"{self.__class__.__name__} with main parameters: Z min -> {self.z_min}; Z max -> {self.z_max}; vertical exxageration -> {self.vertical_exaggeration}"


class GenericPlotParams:

    def __init__(
            self,
            marker: Union[None, str] = PTATT_MARKER_DEFAULT,
            markersize: numbers.Real = PTATT_MARKER_SIZE_DEFAULT,
            color: Union[None, str] = PTATT_COLOR_DEFAULT,
            linestyle: Union[None, str] = LN_MARKER_STYLE_DEFAULT,
            width: Union[None, numbers.Real] = LN_WIDTH_STYLE_DEFAULT,
            alpha: numbers.Real = PTATT_ALPHA_SIZE_DEFAULT,
            plot_type: PointLikePlotTypes = PTATT_PLOT_TYPE_DEFAULT,
            labels: bool = LBLS_PLOT_DEFAULT,
            label_orientations: bool = PTATT_LABELS_ORIENTIONS_DEFAULT,
            label_ids: bool = PTATT_LABELS_IDS_DEFAULT,
    ):

        self.marker = marker
        self.markersize = markersize
        self.color = color
        self.linestyle = linestyle
        self.width = width
        self.alpha = alpha
        self.plot_type = plot_type
        self.labels = labels
        self.label_orientations = label_orientations
        self.label_ids = label_ids


class ElevationPlotParams(GenericPlotParams):

    def __init__(self,
        color: Union[None, str] = "darkgrey",
        width: numbers.Real = 1.0,
        alpha: numbers.Real = 1.0,
    ):

        super(ElevationPlotParams, self).__init__(
            marker=None,
            color=color,
            width=width,
            alpha=alpha,
            labels=False,
        )


class PointPlotParams(GenericPlotParams):

    def __init__(self,
        marker: Union[None, str] = 'o',
        markersize: numbers.Real = 3,
        color: Union[None, str] = "red",
        alpha: numbers.Real = 0.85,
        labels: bool = False,
    ):

        super(PointPlotParams, self).__init__(
            marker=marker,
            markersize=markersize,
            color=color,
            linestyle=None,
            width=None,
            alpha=alpha,
            plot_type=PointLikePlotTypes.POINTS,
            labels=labels,
        )


class AttitudePlotParams(GenericPlotParams):
    """
    Attitude projection params.
    """

    def __init__(self,
                 marker: Union[None, str] = 'o',
                 markersize: numbers.Real = 3,
                 color: Union[None, str] = "red",
                 alpha: numbers.Real = 0.85,
                 labels: bool = True,
                 label_orientations: bool = PTATT_LABELS_ORIENTIONS_DEFAULT,
                 label_ids: bool = PTATT_LABELS_IDS_DEFAULT,
                 ):

        super(AttitudePlotParams, self).__init__(
            marker=marker,
            markersize=markersize,
            color=color,
            linestyle=None,
            width=None,
            alpha=alpha,
            plot_type=PointLikePlotTypes.ATTITUDES,
            labels=labels,
            label_orientations=label_orientations,
            label_ids=label_ids
        )


class FocalMechanismPlotParams:
    """
    Focal mechanism (beachball) plot params, rendered via obspy.imaging.beachball.beach.
    """

    def __init__(self,
                 width: numbers.Real = FMEC_WIDTH_DEFAULT,
                 facecolor: Union[None, str] = FMEC_FACECOLOR_DEFAULT,
                 bgcolor: Union[None, str] = FMEC_BGCOLOR_DEFAULT,
                 edgecolor: Union[None, str] = FMEC_EDGECOLOR_DEFAULT,
                 linewidth: numbers.Real = FMEC_LINEWIDTH_DEFAULT,
                 alpha: numbers.Real = FMEC_ALPHA_DEFAULT,
                 labels: bool = FMEC_LABELS_DEFAULT,
                 view: FocalMechanismView = FMEC_VIEW_DEFAULT,
                 ):

        self.width = width
        self.facecolor = facecolor
        self.bgcolor = bgcolor
        self.edgecolor = edgecolor
        self.linewidth = linewidth
        self.alpha = alpha
        self.labels = labels
        self.view = view
        self.plot_type = PointLikePlotTypes.FOCAL_MECHANISMS


class MapPlotParams:

    def __init__(
        self,
        map_zoom: numbers.Real = 1,
):

        self.map_zoom = map_zoom


class GridPlotParams:

    def __init__(
        self,
        grid_colormap: str = GRID_COLORMAP_DEFAULT,
        hillshade: bool = GRID_USE_HILLSHADE_DEFAULT,
        hs_vert_exagg: numbers.Real = GRID_HS_VERT_EXAGG_DEFAULT,
        hs_blend_mode: str = GRID_HS_BLEND_MODE_DEFAULT,  # one of 'hillshade', 'hsv', 'overlay', 'soft'
        hs_light_source_azim: numbers.Real = GRID_HS_LIGHT_SOURCE_AZIM_DEFAULT,
        hs_light_source_degr: numbers.Real = GRID_HS_LIGHT_SOURCE_DEGR_DEFAULT,
        plot_colorbar: bool = GRID_PLOT_COLORBAR_DEFAULT,
    ):

        self.grid_colormap = grid_colormap
        self.hillshade = hillshade
        self.hs_vert_exagg = hs_vert_exagg
        self.hs_blend_mode = hs_blend_mode
        self.hs_light_source_azim = hs_light_source_azim
        self.hs_light_source_degr = hs_light_source_degr
        self.plot_colorbar = plot_colorbar


class StereonetStyleParams:

    def __init__(
        self,
        line_color: Union[None, str] = None,
        line_style: Union[None, str] = None,
        line_width: Union[None, str] = None,
        line_transp: Union[None, str] = None,
        marker_color: Union[None, str] = None,
        marker_style: Union[None, str] = None,
        marker_size: Union[None, str] = None,
        marker_transp: Union[None, str] = None,
        p_axis_color: Union[None, str] = None,
        t_axis_color: Union[None, str] = None,
        pt_axis_style: Union[None, str] = None,
        pt_axis_size: Union[None, str] = None,
        pt_axis_transp: Union[None, str] = None,
        tl_color: Union[None, str] = None,
        tl_marker_style: Union[None, str] = None,
        tl_marker_size: Union[None, str] = None,
        tl_marker_transp: Union[None, str] = None,
    ):

        self.line_color = line_color if line_color is not None else "#FF0000"
        self.line_style = line_style if line_style is not None else "solid"
        self.line_width = line_width if line_width is not None else "1 pt(s)"
        self.line_transp = line_transp if line_transp is not None else "0%"
        self.marker_color = marker_color if marker_color is not None else "#0000FF"
        self.marker_style = marker_style if marker_style is not None else "circle"
        self.marker_size = marker_size if marker_size is not None else "6 pt(s)"
        self.marker_transp = marker_transp if marker_transp is not None else "0%"
        self.p_axis_color = p_axis_color if p_axis_color is not None else "#FF0000"
        self.t_axis_color = t_axis_color if t_axis_color is not None else "#0000FF"
        self.pt_axis_style = pt_axis_style if pt_axis_style is not None else "circle"
        self.pt_axis_size = pt_axis_size if pt_axis_size is not None else "6 pt(s)"
        self.pt_axis_transp = pt_axis_transp if pt_axis_transp is not None else "0%"
        self.tl_color = tl_color if tl_color is not None else "#008000"
        self.tl_marker_style = tl_marker_style if tl_marker_style is not None else "circle"
        self.tl_marker_size = tl_marker_size if tl_marker_size is not None else "6 pt(s)"
        self.tl_marker_transp = tl_marker_transp if tl_marker_transp is not None else "0%"
