#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-

from typing import NoReturn
import pandas
from abc import ABC, abstractmethod
import matplotlib.pyplot as plt
import matplotlib


class BaseIndicator(ABC):
    """
    An abstract base class for trading indicators. This class provides a structure for defining custom indicators
    and integrating them with market data to generate trading signals. It also includes utilities for visualizing
    these signals on a plot.

    Attributes:
        data (pd.DataFrame): A DataFrame to store market data and generated signals.
    """

    def __init__(self):
        """
        Initializes the BaseIndicator instance.
        """
        self.data = None

    @abstractmethod
    def generate_signal(self) -> pandas.DataFrame:
        """
        An abstract method to be implemented by subclasses for generating trading signals based on market data.

        Returns:
            pd.DataFrame: A DataFrame with trading signals.
        """
        pass

    def __repr__(self) -> str:
        return str(self.__class__.__name__)

    def plot(self) -> NoReturn:
        """
        Plots the trading signals generated by the indicator.

        This method creates a plot with shaded areas indicating buy and sell signals.
        """
        figure, ax = plt.subplots(1, 1, figsize=(12, 4))

        self.add_to_ax(ax)

        if 'signal' in self.data.columns:
            self.shade_signal_area(ax=ax)

        plt.legend()

        plt.show()

    @property
    def signal(self) -> pandas.Series:
        """
        Returns the trading signals generated by the indicator.

        Returns:
            pd.Series: A series containing the trading signals.
        """
        return self.data['signal']

    def post_generate_signal(function):
        """
        A decorator for the `generate_signal` method to initialize the `data` attribute and extract relevant market data.

        Args:
            function: The `generate_signal` method of the subclass.

        Returns:
            The wrapper function.
        """

        def wrapper(self, market_data: pandas.DataFrame):
            self.data = pandas.DataFrame(index=market_data.index)
            self.data[self.value_type] = market_data[self.value_type]

            function(self, market_data=market_data)

            return self.data['signal']

        return wrapper

    def group_signal_per_section(self) -> pandas.DataFrame:
        """
        Groups trading signals to identify continuous sections of the same signal type.

        Returns:
            pd.DataFrame: A DataFrame with grouped signal sections.
        """
        df = pandas.DataFrame(index=self.data.index)
        df['group'] = self.data.signal.ne(self.data.signal.shift()).cumsum()

        df = df.groupby('group')

        return df

    def shade_signal_area(self, ax: plt.Axes) -> NoReturn:
        ax.fill_between(
            x=self.data.index,
            y1=0,
            y2=1,
            where=self.data.signal == -1,
            color='red',
            label='Sell signal',
            alpha=0.2,
            transform=ax.get_xaxis_transform(),
        )

        ax.fill_between(
            x=self.data.index,
            y1=0,
            y2=1,
            where=self.data['signal'] == +1,
            color='green',
            label='Buy signal',
            alpha=0.2,
            transform=ax.get_xaxis_transform(),
        )

    def decorator_shade_signal(function):
        """
        A decorator to add shaded areas to the plot for buy and sell signals.

        Args:
            function: The plotting function to be decorated.

        Returns:
            The wrapper function.
        """

        def wrapper(self, ax: matplotlib.axes.Axes):
            function(self, ax)

            self.shade_signal_area(ax=ax)

        return wrapper

    @decorator_shade_signal
    def add_to_ax(self, ax: matplotlib.axes.Axes) -> NoReturn:
        """
        Adds the indicator's data to the provided Axes object. This method is intended to be overridden by subclasses
        to plot specific indicator data.

        Args:
            ax (plt.Axes): The matplotlib Axes object to which the indicator data will be added.
        """
        pass

# -
