#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import numbers
import math
import matplotlib.pyplot as plt
import numpy as np

# a) und b)

class Polynom:
    ''' Polynom-Klasse '''

    def __init__(self, dp, alg):

        # Entferne alle Terme mit Koeffizienten 0
        self.dp = {k: v for k, v in dp.items() if v != 0}
        # falls dadurch dp leer wird, müssen wri dp auf das Nullpolynom setzen
        if not self.dp:
            self.dp = {0: 0}

        self.alg = alg
        self.degree = max(self.dp.keys())
        # Die Überprüfung der Eingabe wurde in die __call__ Methode von alg verschoben

    def _pstr(self):
        ''' Hilfsfunktion zum Erstellen des Polynomstring 
            Die geschweiften Klammern um den Exponenten brauchen wir 
            für die Legende im Plot
        '''
        polystr = ''
        for k, pk in reversed(sorted(self.dp.items())):
            if isinstance(pk, complex):  # Klammern um komplexe Zahl
                polystr = polystr + f'+({pk:.3f})*X^{{{k}}}'
            elif round(pk) == pk:
                polystr = polystr + f'+{pk:g}*X^{{{k}}}'
            else:
                polystr = polystr + f'+{pk:.3f}*X^{{{k}}}'
        replacements = {"*X^{0}": "", "X^{1}": "X",
                        "+-1*": "-", "+1*": "+", "+-": "-"}
        for k, v in replacements.items():
            polystr = polystr.replace(k, v)
        polystr = polystr.lstrip('+')
        return polystr

    def _getfield(self, a, b):
        '''
        Hilfsfunktion um den Typ der algebraischen Struktur anzupassen.
        So soll die Addition von einem Polynom in C[X] und einem in Z[X] 
        erlaubt sein und ein Polynom in C[X] liefern.
        '''
        if a.field is numbers.Complex or b.field is numbers.Complex:
            field_str_ = 'C'
        elif a.field is numbers.Real or b.field is numbers.Real:
            field_str_ = 'R'
        else:
            field_str_ = 'Z'
        #if hasattr(a, 'dim'): # fuer Vektorraeume
        #    return field_str_, a.dim
        return field_str_
        
    def __repr__(self):
        polystr = self._pstr()
        return f'{__class__.__name__} in  {self.alg}  {polystr}'

    def __str__(self):
        polystr = self._pstr()
        return f'{__class__.__name__} {polystr}'

    def __add__(self, other):
        ''' 
            Addition von Polynomen, damit
            für Polynome p und q  p+q die Summe berechnet
        '''
        if isinstance(other, numbers.Number):
            other = self.alg(other)
        Kdim = self._getfield(self.alg,other.alg)
        alg_ = self.alg.__class__(*Kdim)
        return self.__class__(self.alg.add(self, other), alg_)

    def __radd__(self, other):
        ''' 
            Addition von Polynomen, damit
            für eine Zahl p und ein Polynom q
            auch p+q funktioniert
        '''
        if isinstance(other, numbers.Number):
            other = self.alg(other)
        Kdim = self._getfield(self.alg,other.alg)
        alg_ = self.alg.__class__(*Kdim)            
        return self.__class__(self.alg.add(other, self), alg_)

    def __sub__(self, other):
        if isinstance(other, numbers.Number):
            other = self.alg(other)
        Kdim = self._getfield(self.alg,other.alg)
        alg_ = self.alg.__class__(*Kdim)            
        return self.__class__(self.alg.minus(self, other), alg_)

    def __rsub__(self, other):
        if isinstance(other, numbers.Number):
            other = self.alg(other)
        Kdim = self._getfield(self.alg,other.alg)
        alg_ = self.alg.__class__(*Kdim)
        return self.__class__(self.alg.minus(other, self), alg_)

    def __mul__(self, other):
        if isinstance(other, numbers.Number):
            other = self.alg(other)
        Kdim = self._getfield(self.alg,other.alg)
        alg_ = self.alg.__class__(*Kdim)            
        return self.__class__(self.alg.mul(self, other), alg_)

    def __rmul__(self, other):
        if isinstance(other, numbers.Number):
            other = self.alg(other)
        Kdim = self._getfield(self.alg,other.alg)
        alg_ = self.alg.__class__(*Kdim)        
        return self.__class__(self.alg.mul(other, self), alg_)

    def __neg__(self):
        return Polynom(self.alg.neg(self), self.alg)

    def __eq__(self, other):
        return self.dp == other.dp and self.alg == other.alg

    def __call__(self, x):
        ''' Auswertung eines Polynoms p an der Stelle x mit p(x) '''
        return sum((pk*x**k for k, pk in self.dp.items()))

    def diff(self):
        # Ableitung des Polynoms
        erg = dict()
        if self.degree == 0:
            erg[0] = 0
        else:
            for i in self.dp:
                if i != 0:
                    erg[i-1] = self.dp[i]*i
        return Polynom(erg, self.alg)

    def integrate(self):
        # Stammfunktion des Polynoms
        erg = dict()
        for i in self.dp:
            erg[i+1] = self.dp[i]/(i+1)
        return Polynom(erg, self.alg)

    def integral(self, a, b):
        # Wert des Integrals von a bis b
        intPol = self.integrate()
        return intPol(b) - intPol(a)



class PolyGruppe:
    ''' 
        Polynome als additive Grupppe über Ring/Körper

        PolyGruppe(field_str) 

        field_str darf hier C, R oder Z sein
        für komplexe, reelle bzw. ganze Zahlen
    '''

    def __init__(self, field_str):
        self.field_str = field_str
        if field_str == 'C':
            self.field = numbers.Complex
        elif field_str == 'R':
            self.field = numbers.Real
        elif field_str == 'Z':
            self.field = numbers.Integral
        else:
            raise AssertionError(
                f" 'field_str' {field_str} muss 'C', 'R', oder 'Z' sein")

    def __repr__(self):
        return f'Gruppe ({self.field_str}[X],+)'

    def __call__(self, dp):
        if isinstance(dp, numbers.Number):
            dp = {0: dp}
        self.validate(dp)
        return Polynom(dp, self)

    def validate(self, dp):
        '''Test der Eingaben'''

        assert all(isinstance(x, int) and x >= 0 for x in dp.keys()), \
            "Die Exponenten (keys) müssen natürliche Zahlen (einschl. 0) sein."
        assert all((isinstance(v, self.field) for v in dp.values())), \
            f'Für {self} müssen alle Koeffizienten in {dp} "{self.field.__name__}" sein.'

    def __eq__(self, other):
        return self.field == other.field

    def add(self, p, q):
        ''' Polynomaddition'''
        erg = {n: p.dp.get(n, 0) + q.dp.get(n, 0)
               for n in set(p.dp) | set(q.dp)}
        return erg

    def minus(self, p, q):
        ''' Polynomsubtraktion'''
        erg = {n: p.dp.get(n, 0) - q.dp.get(n, 0)
               for n in set(p.dp) | set(q.dp)}
        return erg

    def neg(self, p):
        ''' additive Inverse '''
        erg = {k: -v for k, v in p.dp.items()}
        return erg


class PolyRing(PolyGruppe):
    ''' Polynome als Ring über Ring/Körper
        C[X], R[X] bzw Z[X]

        PolyRing(field_str) 

        field_str darf hier C, R oder Z sein
        für komplexe, reelle bzw. ganze Zahlen '''

    def __repr__(self):
        return f'{self.field_str}[X]'

    def mul(self, p, q):
        ''' Multiplikation im Polynomring'''
        erg = {}
        for i, pi in p.dp.items():
            for j, qj in q.dp.items():
                ipj = i+j
                if ipj in erg:
                    erg[ipj] += pi*qj
                else:
                    erg[ipj] = pi*qj
        return erg
