# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2025 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Line chart: Display series of data as markers (dots)
connected by straight segments
"""

from pygal.graph.graph import Graph
from pygal.util import alter, cached_property, decorate


class Line(Graph):
    """Line graph class"""

    def __init__(self, *args, **kwargs):
        """Set _self_close as False, it's True for Radar like Line"""
        self._self_close = False
        super(Line, self).__init__(*args, **kwargs)

    @cached_property
    def _values(self):
        """Getter for series values (flattened)"""
        return [
            val[1] for serie in self.series for val in
            (serie.interpolated if self.interpolate else serie.points)
            if val[1] is not None and (not self.logarithmic or val[1] > 0)
        ]

    @cached_property
    def _secondary_values(self):
        """Getter for secondary series values (flattened)"""
        return [
            val[1] for serie in self.secondary_series for val in
            (serie.interpolated if self.interpolate else serie.points)
            if val[1] is not None and (not self.logarithmic or val[1] > 0)
        ]

    def _fill(self, values):
        """Add extra values to fill the line"""
        zero = self.view.y(min(max(self.zero, self._box.ymin), self._box.ymax))

        # Check to see if the data has been padded with "none's"
        # Fill doesn't work correctly otherwise
        end = len(values) - 1
        while end > 0:
            x, y = values[end]
            if self.missing_value_fill_truncation == "either":
                if x is not None and y is not None:
                    break
            elif self.missing_value_fill_truncation == "x":
                if x is not None:
                    break
            elif self.missing_value_fill_truncation == "y":
                if y is not None:
                    break
            else:
                raise ValueError(
                    "Invalid value ({}) for config key "
                    "'missing_value_fill_truncation';"
                    " Use 'x', 'y' or 'either'".format(
                        self.missing_value_fill_truncation
                    )
                )
            end -= 1

        return ([(values[0][0], zero)] + values + [(values[end][0], zero)])

    def line(self, serie, rescale=False):
        """Draw the line serie"""
        serie_node = self.svg.serie(serie)
        if rescale and self.secondary_series:
            points = self._rescale(serie.points)
        else:
            points = serie.points
        view_values = list(map(self.view, points))
        if serie.show_dots:
            for i, (x, y) in enumerate(view_values):
                if None in (x, y):
                    continue
                if self.logarithmic:
                    if points[i][1] is None or points[i][1] <= 0:
                        continue
                if (serie.show_only_major_dots and self.x_labels
                        and i < len(self.x_labels)
                        and self.x_labels[i] not in self._x_labels_major):
                    continue

                metadata = serie.metadata.get(i)
                classes = []
                if x > self.view.width / 2:
                    classes.append('left')
                if y > self.view.height / 2:
                    classes.append('top')
                classes = ' '.join(classes)

                self._confidence_interval(
                    serie_node['overlay'], x, y, serie.values[i], metadata
                )

                dots = decorate(
                    self.svg,
                    self.svg.node(serie_node['overlay'], class_="dots"),
                    metadata
                )

                val = self._format(serie, i)
                alter(
                    self.svg.transposable_node(
                        dots,
                        'circle',
                        cx=x,
                        cy=y,
                        r=serie.dots_size,
                        class_='dot reactive tooltip-trigger'
                    ), metadata
                )
                self._tooltip_data(
                    dots, val, x, y, xlabel=self._get_x_label(i)
                )
                self._static_value(
                    serie_node, val, x + self.style.value_font_size,
                    y + self.style.value_font_size, metadata
                )

        if serie.stroke:
            if self.interpolate:
                points = serie.interpolated
                if rescale and self.secondary_series:
                    points = self._rescale(points)
                view_values = list(map(self.view, points))
            if serie.fill:
                view_values = self._fill(view_values)

            if serie.allow_interruptions:
                # view_values are in form [(x1, y1), (x2, y2)]. We
                # need to split that into multiple sequences if a
                # None is present here

                sequences = []
                cur_sequence = []
                for x, y in view_values:
                    if y is None and len(cur_sequence) > 0:
                        # emit current subsequence
                        sequences.append(cur_sequence)
                        cur_sequence = []
                    elif y is None:  # just discard
                        continue
                    else:
                        cur_sequence.append((x, y))  # append the element

                if len(cur_sequence) > 0:  # emit last possible sequence
                    sequences.append(cur_sequence)
            else:
                # plain vanilla rendering
                sequences = [view_values]
            if self.logarithmic:
                for seq in sequences:
                    for ele in seq[::-1]:
                        y = points[seq.index(ele)][1]
                        if y is None or y <= 0:
                            del seq[seq.index(ele)]
            for seq in sequences:
                self.svg.line(
                    serie_node['plot'],
                    seq,
                    close=self._self_close,
                    class_='line reactive' +
                    (' nofill' if not serie.fill else '')
                )

    def _compute(self):
        """Compute y min and max and y scale and set labels"""
        # X Labels
        if self.horizontal:
            self._x_pos = [
                x / (self._len - 1) for x in range(self._len)
            ][::-1] if self._len != 1 else [.5]  # Center if only one value
        else:
            self._x_pos = [
                x / (self._len - 1) for x in range(self._len)
            ] if self._len != 1 else [.5]  # Center if only one value

        self._points(self._x_pos)

        if self.include_x_axis:
            # Y Label
            self._box.ymin = min(self._min or 0, 0)
            self._box.ymax = max(self._max or 0, 0)
        else:
            self._box.ymin = self._min
            self._box.ymax = self._max

    def _plot(self):
        """Plot the serie lines and secondary serie lines"""
        for serie in self.series:
            self.line(serie)

        for serie in self.secondary_series:
            self.line(serie, True)
