"""
base: essential base classes for a sourdough project
Corey Rayburn Yung <coreyrayburnyung@gmail.com>
Copyright 2020-2021, Corey Rayburn Yung
License: Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0)

Contents:
    Settings
    Filer
    Stage
    Manager
    Component

"""
from __future__ import annotations
import abc
import dataclasses
import pathlib
from typing import (Any, Callable, ClassVar, Dict, Iterable, List, Mapping, 
                    Optional, Sequence, Tuple, Type, Union)

import more_itertools

import sourdough


@dataclasses.dataclass
class Settings(sourdough.quirks.Base, sourdough.Configuration):
    """Loads and stores configuration settings for a Project.

    Args:
        contents (Union[str, pathlib.Path, Mapping[str, Mapping[str, Any]]]): a 
            dict, a str file path to a file with settings, or a pathlib Path to
            a file with settings. Defaults to en empty dict.
        infer_types (bool): whether values in 'contents' are converted to other 
            datatypes (True) or left alone (False). If 'contents' was imported 
            from an .ini file, a False value will leave all values as strings. 
            Defaults to True.
        defaults (Mapping[str, Mapping[str]]): any default options that should
            be used when a user does not provide the corresponding options in 
            their configuration settings. Defaults to a dict with 'general'
            and 'files' section listed in the class annotations.
        skip (Sequence[str]): names of suffixes to skip when constructing nodes
            for a sourdough project.
        library (ClassVar[Library]): related Library instance that will store
            subclasses and allow runtime construction and instancing of those
            stored subclasses.    
            
    """
    contents: Union[str, pathlib.Path, Mapping[str, Mapping[str, Any]]] = (
        dataclasses.field(default_factory = dict))
    infer_types: bool = True
    defaults: Mapping[str, Mapping[str, Any]] = dataclasses.field(
        default_factory = lambda: {'general': {'verbose': False,
                                               'parallelize': False,
                                               'conserve_memery': False},
                                   'files': {'source_format': 'csv',
                                             'interim_format': 'csv',
                                             'final_format': 'csv',
                                             'file_encoding': 'windows-1252'},
                                   'sourdough': {'default_design': 'pipeline',
                                                 'default_workflow': 'graph'}})
    skip: Sequence[str] = dataclasses.field(
        default_factory = lambda: ['general', 
                                   'files', 
                                   'sourdough', 
                                   'parameters'])
    library: ClassVar[sourdough.Library] = sourdough.Library()


@dataclasses.dataclass
class Filer(sourdough.quirks.Base, sourdough.Clerk):
    pass  


@dataclasses.dataclass
class Stage(sourdough.quirks.Base, sourdough.quirks.Needy, abc.ABC):
    """Creates a sourdough object.

    Args:
        needs (ClassVar[Union[Sequence[str], str]]): attributes needed from 
            another instance for some method within a subclass. Defaults to an
            empty list.
        library (ClassVar[Library]): related Library instance that will store
            subclasses and allow runtime construction and instancing of those
            stored subclasses.              
            
    """
    needs: ClassVar[Union[Sequence[str], str]] = []
    library: ClassVar[sourdough.Library] = sourdough.Library()  

    """ Class Methods """
    
    @classmethod
    def create(cls, **kwargs) -> object:
        """[summary]

        Raises:
            ValueError: [description]

        Returns:
            object: [description]
            
        """
        needs = list(more_itertools.always_iterable(cls.needs))
        method = getattr(cls, f'from_{needs[0]}')
        for need in needs:
            if need not in kwargs:
                raise ValueError(
                    f'The create method must include a {need} argument')
        return method(**kwargs)    
     
            
@dataclasses.dataclass
class Manager(sourdough.quirks.Base, sourdough.quirks.Element):
    """

    Args:
        name (str): designates the name of a class instance that is used for 
            internal referencing throughout sourdough. For example, if a 
            sourdough instance needs settings from a Configuration instance, 
            'name' should match the appropriate section name in a Configuration 
            instance. Defaults to None. 
        library (ClassVar[Library]): related Library instance that will store
            subclasses and allow runtime construction and instancing of those
            stored subclasses.
                
    """
    name: str = None
    contents: Any = None
    workflow: sourdough.Structure = None
    needs: ClassVar[Union[Sequence[str], str]] = ['outline', 'name']
    library: ClassVar[sourdough.Library] = sourdough.Library()
    
    """ Public Class Methods """

    @classmethod
    def create(cls, **kwargs) -> object:
        """[summary]

        Raises:
            ValueError: [description]

        Returns:
            object: [description]
            
        """
        needs = list(more_itertools.always_iterable(cls.needs))
        method = getattr(cls, f'from_{needs[0]}')
        for need in needs:
            if need not in kwargs:
                raise ValueError(
                    f'The create method must include a {need} argument')
        return method(**kwargs)   
    
    @classmethod
    def from_outline(cls, outline: Stage, name: str, **kwargs) -> Manager:
        """[summary]

        Args:
            outline (Stage): [description]
            name (str): [description]

        Returns:
            Manager: [description]
            
        """        
        names = [name]
        if name in outline.designs:
            names.append(outline.design[name])
        manager = cls.library.borrow(names = names)
        structure = cls.bases.stage.library.borrow(names = 'workflow')
        workflow = structure.create(outline = outline, name = name)
        return manager(name = name, workflow = workflow, **kwargs)          

    """ Public Methods """
    
    def execute(self, data: Any) -> Any:
        """[summary]

        Args:
            data (Any): [description]

        Returns:
            Any: [description]
        """        
        for node in self.workflow:
            data = node.execute(data = data)
        return data
                
  
@dataclasses.dataclass
class Component(sourdough.quirks.Base, sourdough.quirks.Element, abc.ABC):
    """Base class for parts of a sourdough Workflow.

    Args:
        name (str): designates the name of a class instance that is used for 
            internal referencing throughout sourdough. For example, if a 
            sourdough instance needs settings from a Configuration instance, 
            'name' should match the appropriate section name in a Configuration 
            instance. Defaults to None.
        contents (Any): stored item(s) for use by a Component subclass instance.
        library (ClassVar[Library]): related Library instance that will store
            subclasses and allow runtime construction and instancing of those
            stored subclasses.    
                
    """
    name: str = None
    contents: Any = None
    iterations: Union[int, str] = 1
    parameters: Mapping[Any, Any] = dataclasses.field(default_factory = dict)
    parallel: ClassVar[bool] = False
    library: ClassVar[sourdough.Library] = sourdough.Library()
    
    """ Public Class Methods """
    
    @classmethod
    def create(cls, source: str, **kwargs) -> Component:
        """[summary]

        Args:
            source (str): [description]

        Raises:
            NotImplementedError: [description]

        Returns:
            Component: [description]
            
        """
        if (isinstance(source, str)
                or (isinstance(source, Sequence) and all(source, str))):
            return cls.from_name(name = source, **kwargs)
        elif isinstance(source, cls.bases.Stage):
            return 
  
    @classmethod
    def from_outline(cls, name: str, outline: sourdough.project.Outline, 
                     **kwargs) -> Component:
        """[summary]

        Args:
            name (str): [description]
            section (str): [description]
            outline (sourdough.project.Outline): [description]

        Returns:
            Component: [description]
        """              
        if name in outline.initialization:
            parameters = outline.initialization[name]
            parameters.update(kwargs)
        else:
            parameters = kwargs
        component = cls.library.borrow(names = [name, outline.designs[name]])
        instance = component(name = name, **parameters)
        return instance

    """ Public Methods """
    
    def execute(self, data: Any, **kwargs) -> Any:
        """[summary]

        Args:
            data (Any): [description]

        Returns:
            Any: [description]
            
        """ 
        if self.iterations in ['infinite']:
            while True:
                data = self.implement(data = data, **kwargs)
        else:
            for iteration in range(self.iterations):
                data = self.implement(data = data, **kwargs)
        return data

    def implement(self, data: Any, **kwargs) -> Any:
        """[summary]

        Args:
            data (Any): [description]

        Returns:
            Any: [description]
            
        """  
        if self.parameters:
            parameters = self.parameters
            parameters.update(kwargs)
        else:
            parameters = kwargs
        if self.contents not in [None, 'None', 'none']:
            data = self.contents.implement(data = data, **parameters)
        return data

    """ Private Methods """
    
    def _add_attributes(self, attributes: Dict[Any, Any]) -> None:
        """[summary]

        Args:
            attributes (Dict[Any, Any]): [description]

        Returns:
            [type]: [description]
            
        """
        for key, value in attributes.items():
            setattr(self, key, value)
        return self
    