from typing import Dict
from typing import List
from typing import cast
from typing import NewType

from logging import Logger
from logging import getLogger

from datetime import datetime

from pyutmodel.PyutClass import PyutClass
from pyutmodel.PyutField import PyutField
from pyutmodel.PyutMethod import PyutMethod
from pyutmodel.PyutParameter import PyutParameter
from pyutmodel.PyutType import PyutType

from pyutmodel.PyutVisibilityEnum import PyutVisibilityEnum

MethodsCodeType = NewType('MethodsCodeType', Dict[str, List[str]])


class PyutToPython:

    MAX_WIDTH:            int = 120
    CLASS_COMMENTS_START: str = '"""'
    CLASS_COMMENTS_END:   str = '"""'
    SINGLE_TAB:           str = '    '

    SPECIAL_PYTHON_CONSTRUCTOR: str = '__init__'

    """
    Reads the Pyut data model in order to generated syntactically correct Python code
    """
    def __init__(self):

        self.logger: Logger = getLogger(__name__)

    def generateTopCode(self) -> List[str]:

        now = datetime.now()

        dateTimeStr: str = now.strftime("%Y-%m-%d %H:%M:%S")
        topCode: List[str] = [
            f'# \n',
            f'# Generated by PyutToPython 1.0.0\n',
            f'# -- La Vida Buena, LLC\n',
            f'# {dateTimeStr}\n',
            f'# \n'
            ]

        return topCode

    def generateClassStanza(self, pyutClass: PyutClass) -> str:
        """
        Generates something like this

        ```python
            class Car:
        ```
        or with inheritance

        ```python
            class ElectricCar(BaseVehicle, Car):
        ```
`
        Args:
            pyutClass:   The data model class

        Returns:
            The Python class start stanza
        """
        generatedCode:     str             = f'class {pyutClass.name}'
        parentPyutClasses: List[PyutClass] = cast(List[PyutClass], pyutClass.getParents())

        if len(parentPyutClasses) > 0:  # Add parents
            generatedCode = f'{generatedCode}('
            for i in range(len(parentPyutClasses)):
                generatedCode = f'{generatedCode}{parentPyutClasses[i].name}'
                if i < len(parentPyutClasses) - 1:
                    generatedCode = f'{generatedCode},'
            generatedCode = f'{generatedCode})'
        generatedCode = f'{generatedCode}:\n'
        generatedCode = f'{generatedCode}{self.__indentStr(PyutToPython.CLASS_COMMENTS_START)}\n'
        generatedCode = f'{generatedCode}{self.__indentStr(pyutClass.description)}\n'   # TODO need to split lines according to MAX_WIDTH
        generatedCode = f'{generatedCode}{self.__indentStr(PyutToPython.CLASS_COMMENTS_END)}\n'

        return generatedCode

    def generateMethodsCode(self, pyutClass: PyutClass) -> MethodsCodeType:
        """
        Return a dictionary of method code for a given class

        Args:
            pyutClass:  The data model class for which we have to generate a bunch of code

        Returns:
            A bunch of code that is the code for this class; The map key is the method name the
            value is a list of the method code
        """
        clsMethods: MethodsCodeType = MethodsCodeType({})
        for pyutMethod in pyutClass.methods:
            # Separation
            txt = ""
            lstCodeMethod = [txt]

            # Get code
            subCode:       List[str] = self.generateASingleMethodsCode(pyutMethod)
            lstCodeMethod += self.indent(subCode)

            clsMethods[pyutMethod.name] = lstCodeMethod

        # Create method __init__ if it does not exist
        if PyutToPython.SPECIAL_PYTHON_CONSTRUCTOR not in clsMethods:
            clsMethods = self.generatePythonInitMethod(clsMethods)

        if len(pyutClass.fields) > 0:
            # Add fields

            clsInitMethod: List[str] = clsMethods[PyutToPython.SPECIAL_PYTHON_CONSTRUCTOR]
            for pyutField in pyutClass.fields:
                clsInitMethod.append(self.__indentStr(self.__indentStr(self.generateFieldPythonCode(pyutField))))
            # This is the actual entry;  So no need to put it back in clsMethods
            clsInitMethod.append('\n')

        return clsMethods

    def generatePythonInitMethod(self, clsMethods: MethodsCodeType) -> MethodsCodeType:
        """

        Args:
            clsMethods:  The current in progress dictionary

        Returns:  The updated dictionary
        """
        lstCodeMethod = []
        subCode = self.generateASingleMethodsCode(PyutMethod(PyutToPython.SPECIAL_PYTHON_CONSTRUCTOR), False)

        for el in self.indent(subCode):
            lstCodeMethod.append(str(el))

        clsMethods[PyutToPython.SPECIAL_PYTHON_CONSTRUCTOR] = lstCodeMethod

        return clsMethods

    def generateASingleMethodsCode(self, pyutMethod: PyutMethod, writePass: bool = True) -> List[str]:
        """
        Generate the Python code for the input method

        Args:
            pyutMethod:    The PyutMethod for which we will generate code
            writePass:  If `True` write `pass` in the code

        Returns:
            A list that is the generated code
        """
        methodCode:  List[str] = []

        currentCode: str = self._generateMethodDefinitionStanza(pyutMethod)
        # Add parameters (parameter, parameter, parameter, ...)
        params = pyutMethod.parameters
        currentCode = self._generateParametersCode(currentCode, params)
        currentCode = f'{currentCode})'

        returnType: PyutType = pyutMethod.returnType
        if returnType is not None and returnType.value != '':
            currentCode = f'{currentCode} -> {returnType.value}'

        currentCode = f'{currentCode}:\n'

        # Add to the method code
        methodCode.append(currentCode)

        # Add comments
        methodCode.append(self.__indentStr('"""\n'))
        methodCode = self._generateMethodComments(methodCode, pyutMethod)
        methodCode.append(self.__indentStr('"""\n'))

        if writePass:
            methodCode.append(self.__indentStr('pass\n'))

        methodCode.append('\n')
        return methodCode

    def generateFieldPythonCode(self, pyutField: PyutField):
        """
        Generate the Python code for a given field

        Args:
            pyutField:   The PyutField that is the source of our code generation

        Returns:
            Python Code !!
        """
        fieldCode: str = "self."

        fieldCode = f'{fieldCode}{self.generateVisibilityPrefix(pyutField.visibility)}'
        if pyutField.type.value == '':
            fieldCode = f'{fieldCode}{pyutField.name}'
        else:
            fieldCode = f'{fieldCode}{pyutField.name}: {pyutField.type}'

        value = pyutField.defaultValue
        if value == '':
            fieldCode = f'{fieldCode} = None'
        else:
            fieldCode = f'{fieldCode} = {value}'

        fieldCode = f'{fieldCode}\n'
        return fieldCode

    def generateVisibilityPrefix(self, visibility: PyutVisibilityEnum) -> str:
        """
        Return the python code for the given enumeration value

        Args:
            visibility:

        Returns:
            The Python code that by convention depicts `method` or `field` visibility
        """
        code: str = ''
        if visibility == PyutVisibilityEnum.PUBLIC:
            code = ''
        elif visibility == PyutVisibilityEnum.PROTECTED:
            code = '_'
        elif visibility == PyutVisibilityEnum.PRIVATE:
            code = '__'
        else:
            self.logger.error(f"PyutToPython: Field code not supported : {visibility}")
        self.logger.debug(f"Python code: {code}, for {visibility}")
        return code

    def _generateMethodDefinitionStanza(self, pyutMethod: PyutMethod):
        """
        Follow Python conventions for method visibility

        Something like:
        ```python
            def publicMethod(self)
            def _protectedMethod(self)
            def __privateMethod(self)
        ```
        Args:
            pyutMethod: The method whose code we are generating

        Returns:
            The method start stanza
        """
        currentCode: str = "def "
        if self.__isDunderMethod(pyutMethod.name) is False:
            currentCode = f'{currentCode}{self.generateVisibilityPrefix(pyutMethod.getVisibility())}'
        currentCode = f'{currentCode}{pyutMethod.name}(self'

        return currentCode

    def _generateParametersCode(self, currentCode: str, params: List[PyutParameter]):

        if len(params) > 0:
            currentCode = f'{currentCode}, '
        # Add parameter code
        for i in range(len(params)):
            pyutParam: PyutParameter = params[i]
            numParams: int = len(params)
            paramCode: str = self.__generateParameter(currentParamNumber=i, numberOfParameters=numParams, pyutParam=pyutParam)

            currentCode = self.__addParamToMethodSignature(currentCode, paramCode)
        return currentCode

    def __generateParameter(self, currentParamNumber: int, numberOfParameters: int, pyutParam: PyutParameter) -> str:
        """

        Args:
            currentParamNumber: The current parameter #
            numberOfParameters: The number of parameters the method has
            pyutParam:          What we are generating code from

        Returns:
            Python code for a single parameter
        """

        paramCode: str = ""

        paramCode = f'{paramCode}{pyutParam.name}'

        paramType: PyutType = pyutParam.type
        if paramType is not None and paramType.value != '':
            paramCode = f'{paramCode}: {paramType.value}'
        if pyutParam.defaultValue is not None:
            paramCode = f'{paramCode} = {pyutParam.defaultValue}'
        if currentParamNumber < numberOfParameters - 1:
            paramCode = f'{paramCode}, '

        return paramCode

    def _generateMethodComments(self, methodCode, pyutMethod):

        methodCode.append(self.__indentStr('(TODO : add description)\n\n'))

        params: List[PyutParameter] = pyutMethod.parameters

        if len(params) > 0:
            methodCode.append(self.__indentStr(f'Args:\n'))

        for i in range(len(params)):
            param: PyutParameter = params[i]
            methodCode.append(self.__indentStr(f'{param.name}:\n', 2))

        # Add others
        returnType: PyutType = pyutMethod.returnType
        if returnType is not None and len(str(returnType)) > 0:
            methodCode.append(self.__indentStr('Returns:\n'))
            methodCode.append(self.__indentStr(f'{returnType}\n', 2))

        return methodCode

    def __addParamToMethodSignature(self, currentCode, paramCode):
        """
        Is smart enough to know if the parameters list is so long it must be indented

        Args:
            currentCode:    Generated 'so far` code
            paramCode:      Generated code for current parameter

        Returns:
            Updated code
        """
        if (len(currentCode) % PyutToPython.MAX_WIDTH) + len(paramCode) > PyutToPython.MAX_WIDTH:  # Width limit
            currentCode += "\n" + self.__indentStr(self.__indentStr(paramCode))
        else:
            currentCode = f'{currentCode}{paramCode}'
        return currentCode

    def __indentStr(self, stringToIndent: str, numTabs: int = 1) -> str:
        """
        Indent one string by one unit

        Args:
            stringToIndent:  string to indent
            numTabs:         number of tabs to insert

        Returns:
            Indented string
        """
        insertedTabs: str = ''
        for x in range(numTabs):
            insertedTabs = f'{insertedTabs}{PyutToPython.SINGLE_TAB}'

        return f'{insertedTabs}{stringToIndent}'

    def __isDunderMethod(self, methodName: str) -> bool:
        """
        Actually, checks to see if a method name already has leading or trailing underscore(s);

        Returns:  `True` if the names starts with underscores else `False`
        """
        isDunder: bool = False
        if methodName.startswith('_') or methodName.startswith('__'):
            isDunder = True
        return isDunder

    def indent(self, listIn: List[str]) -> List[str]:
        """
        Indent every line in listIn by one unit

        Args:
            listIn: Many strings

        Returns:
            A new list with indented strings
        """
        listOut: List[str] = []
        for el in listIn:
            listOut.append(self.__indentStr(str(el)))
        return listOut
