__version__ = '0.1.2'

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import os
import sys 

def Generator():
    """
    Factory function to create and return an instance of the Types class.

    This function serves as the primary entry point for the charting library, 
    allowing users to access various chart generation methods (Bar, Pie, Line, etc.) 
    via the returned object.

    Returns:
        Types: An instance of the Types class, which contains all chart methods.
    """
    class Types:
        """
        A comprehensive class for generating various types of plots using Matplotlib 
        and Pandas.

        All chart methods share a common internal utility, `_handle_save_and_show`, 
        to manage chart display and file saving.
        """
        def __init__(self):
            """
            Initializes the Types class.
            
            No complex setup is required during initialization.
            """
            pass

        def _handle_save_and_show(self, chart_title, save_as=None, show=True):
            """
            Internal helper function to handle saving the plot to a file and 
            controlling its display.

            This function is called by all public chart methods immediately before 
            the plot is displayed or closed. It handles file path validation and 
            error reporting for saving.

            Args:
                chart_title (str): The title of the chart (used for context, not display here).
                save_as (str or None, optional): The file path to save the chart. If a string 
                    is provided, the chart is saved. Defaults to None.
                show (bool, str or int, optional): Controls whether the chart is displayed. 
                    Can be True, False, 'no', 'false', 0, or 1. Defaults to True.
            """
            
            # --- 1. Handle Saving ---
            save_path = None
            if save_as is not None:
                # Convert non-string types to string, handling common iterable types
                if isinstance(save_as, (list, tuple, set, dict)):
                    try:
                        # Get the first item if it's an iterable with items
                        if len(save_as) > 0:
                            save_path = str(list(save_as)[0])
                    except Exception:
                        # Fallback for empty/unhandled iterables
                        save_path = str(save_as)
                elif isinstance(save_as, (int, float)):
                    save_path = str(save_as)
                elif isinstance(save_as, str):
                    save_path = save_as
                
                # Ensure a file extension is present (default to .png if missing)
                if save_path and '.' not in os.path.basename(save_path):
                    save_path += '.png'
                    
                # Save the figure
                if save_path:
                    try:
                        plt.savefig(save_path, bbox_inches='tight')
                    except Exception as e:
                        print(f"Error saving chart to {save_path}: {e}", file=sys.stderr)

            # --- 2. Handle Showing ---
            display_chart = True
            if isinstance(show, str):
                if show.lower() in ('no', 'false'):
                    display_chart = False
            elif show in (False, 0): # Check for boolean False or integer 0
                display_chart = False
            
            if display_chart:
                plt.show()
            else:
                # Close the figure without showing to free memory/resources
                plt.close()


        def Bar(self,
                alignment='vertical',
                category_label='Categories',
                amount_label='Amount in Categories',
                chart_title='My Bar Chart',
                categories_in_order=['Category 1', 'Category 2', 'Category 3'],
                amounts_in_order=[1, 2, 3],
                colors_in_order=['blue', 'red', 'green'],
                show=True,
                save_as=None):
            """
            Generates and displays a Matplotlib Bar Chart (vertical or horizontal).

            This method creates a bar chart, labels the axes, sets the title, and 
            adds data labels above or next to the bars.

            Args:
                alignment (str, optional): The orientation of the bars. 
                    Must be 'vertical' or 'horizontal'. Defaults to 'vertical'.
                category_label (str, optional): The label for the category axis (x-axis for vertical, y-axis for horizontal). 
                    Defaults to 'Categories'.
                amount_label (str, optional): The label for the amount axis (y-axis for vertical, x-axis for horizontal). 
                    Defaults to 'Amount in Categories'.
                chart_title (str, optional): The main title of the chart. 
                    Defaults to 'My Bar Chart'.
                categories_in_order (list[str], optional): A list of category names (bar labels). 
                    Defaults to ['Category 1', 'Category 2', 'Category 3'].
                amounts_in_order (list[int | float], optional): A list of numerical values corresponding to the categories. 
                    Defaults to [1, 2, 3].
                colors_in_order (list[str], optional): A list of colors for each bar. 
                    Should match the length of `categories_in_order`. Defaults to ['blue', 'red', 'green'].
                show (bool, str or int, optional): Controls whether the chart is displayed. 
                    Defaults to True.
                save_as (str or None, optional): The file path to save the chart. 
                    Defaults to None.
            """
            data = {
                category_label: categories_in_order,
                amount_label: amounts_in_order
            }
            
            df = pd.DataFrame(data)

            plt.figure(figsize=(14, 8))
            
            max_amount = df[amount_label].max()

            if alignment.lower() == 'horizontal':
                # Horizontal Bar Chart
                bars = plt.barh(df[category_label], df[amount_label], color=colors_in_order)

                plt.title(chart_title, fontsize=16)
                plt.ylabel(category_label, fontsize=12)
                plt.xlabel(amount_label, fontsize=12)

                for bar in bars:
                    xval = bar.get_width()
                    plt.text(xval + max_amount * 0.02, bar.get_y() + bar.get_height()/2, int(xval), ha='left', va='center', fontsize=10)

                plt.xlim(0, max_amount * 1.1)
                
            elif alignment.lower() == 'vertical':
                # Vertical Bar Chart
                bars = plt.bar(df[category_label], df[amount_label], color=colors_in_order)

                plt.title(chart_title, fontsize=16)
                plt.xlabel(category_label, fontsize=12)
                plt.ylabel(amount_label, fontsize=12)

                for bar in bars:
                    yval = bar.get_height()
                    plt.text(bar.get_x() + bar.get_width()/2, yval + max_amount * 0.02, int(yval), ha='center', va='bottom', fontsize=10)

                plt.ylim(0, max_amount * 1.1)
                
            else:
                print(f"Error: Alignment '{alignment}' not recognized. Use 'vertical' or 'horizontal'.", file=sys.stderr)
                plt.close()
                return # Exit the function if alignment is invalid

            # Handle saving and showing
            self._handle_save_and_show(chart_title, save_as, show)


        def Pie(self,
                amounts_in_order=[40, 30, 20, 10],
                categories_in_order=['Apples', 'Bananas', 'Oranges', 'Grapes'],
                chart_title='Fruit Distribution',
                colors_in_order=['#ff9999','#66b3ff','#99ff99','#ffcc99'], # Pastel colors
                split=[0, 0.1, 0, 0],
                make_shadow=True,
                show=True,
                save_as=None):
            
            """
            Generates and displays a Matplotlib Pie Chart.

            The method supports setting custom colors, making one or more slices 
            'explode' (split), and showing percentage labels.

            Args:
                amounts_in_order (list[int | float], optional): The numerical size of each slice. 
                    These values are used to calculate percentages. Defaults to [40, 30, 20, 10].
                categories_in_order (list[str], optional): The labels for each slice/wedge. 
                    Defaults to ['Apples', 'Bananas', 'Oranges', 'Grapes'].
                chart_title (str, optional): The main title of the chart. 
                    Defaults to 'Fruit Distribution'.
                colors_in_order (list[str], optional): A list of colors (names or hex codes) for the wedges. 
                    Defaults to pastel colors.
                split (list[int | float], optional): A list of values representing how far each wedge 
                    should be separated from the center (exploded). Defaults to [0, 0.1, 0, 0].
                make_shadow (bool, optional): If True, a shadow is drawn beneath the pie. 
                    Defaults to True.
                show (bool, str or int, optional): Controls whether the chart is displayed. 
                    Defaults to True.
                save_as (str or None, optional): The file path to save the chart. 
                    Defaults to None.
            """

            plt.figure(figsize=(8, 8))
            
            # The pie function is the core of the pie chart
            plt.pie(amounts_in_order, 
                    labels=categories_in_order, 
                    colors=colors_in_order,
                    # autopct formats the percentage display inside the wedges
                    autopct='%1.1f%%', 
                    shadow=make_shadow, 
                    startangle=90, 
                    explode=split,
                    # pctdistance sets the distance of percentage text from the center
                    pctdistance=0.85)

            # Equal aspect ratio ensures that pie is drawn as a circle.
            plt.axis('equal')  
            
            plt.title(chart_title, fontsize=16)
            
            # Handle saving and showing
            self._handle_save_and_show(chart_title, save_as, show)


        def Line(self,
                 x_data,
                 y_data_sets, 
                 x_label='Time (X-Axis)',
                 y_label='Value (Y-Axis)',
                 chart_title='My Line Graph',
                 data_labels=None, 
                 show=True,
                 save_as=None):
            
            """
            Generates and displays a Matplotlib Line Graph supporting multiple data sets.

            Each data set is plotted as a separate line, allowing for comparison over 
            a common x-axis. A legend is automatically included for multiple series.

            Args:
                x_data (list[int | float]): The data points for the horizontal axis (X-axis). 
                    All lines will share this common X-axis data.
                y_data_sets (list[dict]): A list where each dictionary represents a single line series. 
                    Each dictionary must contain a 'y_data' key and can optionally include 
                    'line_color', 'line_style', 'marker_style', and 'label'.
                x_label (str, optional): The label for the X-axis. Defaults to 'Time (X-Axis)'.
                y_label (str, optional): The label for the Y-axis. Defaults to 'Value (Y-Axis)'.
                chart_title (str, optional): The main title of the chart. Defaults to 'My Line Graph'.
                data_labels (list[str] or None, optional): Explicit labels for the legend, 
                    overriding any labels found in `y_data_sets`. Defaults to None.
                show (bool, str or int, optional): Controls whether the chart is displayed. 
                    Defaults to True.
                save_as (str or None, optional): The file path to save the chart. 
                    Defaults to None.
            """
            
            if not isinstance(y_data_sets, list):
                print("Error: 'y_data_sets' must be a list of dictionaries.", file=sys.stderr)
                return

            plt.figure(figsize=(10, 6))
            
            # Iterate over the list of line data dictionaries
            for i, line_data in enumerate(y_data_sets):
                # Safely get y_data, defaulting to an empty list if missing
                y_data = line_data.get('y_data', [])
                
                # Safely get optional parameters, providing defaults
                line_color = line_data.get('line_color', 'black')
                line_style = line_data.get('line_style', '-')
                marker_style = line_data.get('marker_style', 'o')
                
                # Get the label for the legend, either from a dedicated list or line_data
                if data_labels and i < len(data_labels):
                    label = data_labels[i]
                else:
                    # Fallback label generation
                    label = line_data.get('label', f'Series {i+1}')

                # Plot the line graph
                plt.plot(x_data, y_data, 
                         color=line_color, 
                         linestyle=line_style, 
                         marker=marker_style, 
                         linewidth=2,
                         label=label) # Use label for the legend

            plt.title(chart_title, fontsize=16)
            plt.xlabel(x_label, fontsize=12)
            plt.ylabel(y_label, fontsize=12)
            
            # Add legend only if there's more than one series or explicit labels were provided
            if len(y_data_sets) > 1 or data_labels:
                 plt.legend(loc='best')
            
            plt.grid(True, linestyle='--', alpha=0.7)
            
            # Handle saving and showing
            self._handle_save_and_show(chart_title, save_as, show)


        def Scatter(self,
                    x_data=[1, 2, 3, 4, 5],
                    y_data=[10, 12, 5, 15, 8],
                    x_label='Independent Variable',
                    y_label='Dependent Variable',
                    chart_title='My Scatter Plot',
                    marker_color='red',
                    marker_style='x',
                    marker_size=100,
                    show=True,
                    save_as=None):
            
            """
            Generates and displays a Matplotlib Scatter Plot.

            A scatter plot displays values for two variables for a set of data. 
            The method allows customization of marker appearance.

            Args:
                x_data (list[int | float], optional): The data points for the X-axis. 
                    Defaults to [1, 2, 3, 4, 5].
                y_data (list[int | float], optional): The data points for the Y-axis. 
                    Must match the length of `x_data`. Defaults to [10, 12, 5, 15, 8].
                x_label (str, optional): The label for the X-axis. 
                    Defaults to 'Independent Variable'.
                y_label (str, optional): The label for the Y-axis. 
                    Defaults to 'Dependent Variable'.
                chart_title (str, optional): The main title of the chart. 
                    Defaults to 'My Scatter Plot'.
                marker_color (str, optional): The color of the data points. 
                    Defaults to 'red'.
                marker_style (str, optional): The shape of the data points (e.g., 'o', 'x', '^'). 
                    Defaults to 'x'.
                marker_size (int | float, optional): The size of the data points in points squared. 
                    Defaults to 100.
                show (bool, str or int, optional): Controls whether the chart is displayed. 
                    Defaults to True.
                save_as (str or None, optional): The file path to save the chart. 
                    Defaults to None.
            """
            
            plt.figure(figsize=(10, 6))
            
            # Plot the scatter plot
            plt.scatter(x_data, y_data, 
                        color=marker_color, 
                        marker=marker_style, 
                        s=marker_size) # s is for size

            plt.title(chart_title, fontsize=16)
            plt.xlabel(x_label, fontsize=12)
            plt.ylabel(y_label, fontsize=12)
            
            plt.grid(True, linestyle=':', alpha=0.6)

            # Handle saving and showing
            self._handle_save_and_show(chart_title, save_as, show)


        def Histogram(self,
                      data_to_plot=np.random.normal(loc=50, scale=10, size=1000),
                      number_of_bins=30,
                      data_label='Distribution of Values',
                      chart_title='My Histogram',
                      bar_color='teal',
                      edge_color='black',
                      show=True,
                      save_as=None):
            
            """
            Generates and displays a Matplotlib Histogram.

            A histogram shows the distribution of a single numerical variable by 
            binning data and counting frequency.

            Args:
                data_to_plot (np.ndarray or list[int | float], optional): The numerical data array 
                    to be plotted. Defaults to 1000 samples from a normal distribution 
                    ($\mu=50, \sigma=10$).
                number_of_bins (int, optional): The number of bins to divide the data range into. 
                    Defaults to 30.
                data_label (str, optional): The label for the X-axis (the data values). 
                    Defaults to 'Distribution of Values'.
                chart_title (str, optional): The main title of the chart. 
                    Defaults to 'My Histogram'.
                bar_color (str, optional): The fill color of the histogram bars. 
                    Defaults to 'teal'.
                edge_color (str, optional): The border color of the histogram bars. 
                    Defaults to 'black'.
                show (bool, str or int, optional): Controls whether the chart is displayed. 
                    Defaults to True.
                save_as (str or None, optional): The file path to save the chart. 
                    Defaults to None.
            """
            
            plt.figure(figsize=(10, 6))
            
            # The hist function calculates and plots the histogram
            plt.hist(data_to_plot, 
                     bins=number_of_bins, 
                     color=bar_color, 
                     edgecolor=edge_color,
                     alpha=0.7) # alpha for transparency

            plt.title(chart_title, fontsize=16)
            plt.xlabel(data_label, fontsize=12)
            plt.ylabel('Frequency', fontsize=12)
            
            plt.grid(axis='y', alpha=0.5)

            # Handle saving and showing
            self._handle_save_and_show(chart_title, save_as, show)


    return Types()

chart = Generator()

__all__ = [chart]
