class PolynomialSolver:

    def __init__(self):
        self.order=None
        self.co_eff=None
        self.interval=None
        self.initial_guess=None
        self.max_iterations=None
        self.accuracy=None

    def solve(self,order,co_eff,interval,method,initial_guess1=[-1,1],initial_guess2=1,max_iterations=10000,accuracy=0.0001):

        # initial_guess1 is a list of two starting values for Secant and SecantRF methods, e.g.- [1,3]
        # initial_guess2 is the starting value for NewtonRaphson method, e.g.- 1 
        # initial_guess1, initial_guess2, max_iterations and accuracy are optional arguments

        if(method=="bisection"):
            return (self.BisectionSearch(co_eff,interval,max_iterations,accuracy))
        elif(method=="secant"):
            return (self.Secant(co_eff,initial_guess1,max_iterations,accuracy))
        elif(method=="secantrf"):
            return (self.SecantRF(co_eff,initial_guess1,max_iterations,accuracy))
        else:
            return (self.NewtonRaphson(co_eff,initial_guess2,max_iterations,accuracy))

    def BisectionSearch(self,L,interval,max_iterations,accuracy):           # BisectionSearch function takes 5 arguments
        
        # L is the co-efficient list, e.g.,-
        # If the polynomial is x^2-7x+10, then-
        # L=[10,-7,1]
        # interval is the Interval in which the roots are searched, e.g-
        # interval=[3,6] ; 3-lower limit, 6-upper limit
        # max_iterations is the maximum allowed number of iterations
        # accuracy is the desired Accuracy
        
        self.co_eff=L
        self.interval=interval
        self.max_iterations=max_iterations
        self.accuracy=accuracy
        count=0                                                             # variable to count number of iterations

        x=list(frange(self.interval[0],self.interval[1]))
        y=list(map(lambda X:P.polyval(X,self.co_eff),x))
        plt.plot(x,y,linewidth=2)                                           # plotting polynomial
        plt.axhline(0,color="k",linewidth=2)
        plt.plot(self.interval,[-30,-30],color="r")                         # plotting interval range
        plt.xlabel("x values")
        plt.ylabel("f(x) values")
        Legend=[]                                                           # initialising legend     
        Legend.append(mpatches.Patch(color="b",label="Polynomial graph"))   # updating legend
        Legend.append(mpatches.Patch(color="r",label="Interval selected"))
        plt.legend(handles=Legend)                                          # putting legend on the plot

        while(count<self.max_iterations):
            if(P.polyval(self.interval[0],self.co_eff)*P.polyval(self.interval[1],self.co_eff)<0 and self.interval[1]-self.interval[0]<=self.accuracy):  
                plt.show()                                                  # displaying plot
                return (self.interval)                                      # return interval (terminal condition)
            m=(self.interval[0]+self.interval[1])/2
            if(P.polyval(self.interval[0],self.co_eff)*P.polyval(m,self.co_eff)<0):
                self.interval=[self.interval[0],m]                          # bisection
            else:
                self.interval=[m,self.interval[1]]                          # bisection
            self.plot((count+2)*30)
            count+=1
        return ("Iterations exceed limit")
    
    def plot(self,temp):
        plt.plot(self.interval,[-1*temp,-1*temp],color="r")                 # plotting current interval range
        plt.plot([self.interval[0],self.interval[0]],[-1*temp-5,-1*temp+5],color="r")
        plt.plot([self.interval[1],self.interval[1]],[-1*temp-5,-1*temp+5],color="r")
        
    def Secant(self,L,initial_guess,max_iterations,accuracy):               # Secant function takes 5 arguments
        
        # L is the co-efficient list, e.g.,-
        # If the polynomial is x^2-7x+10, then-
        # L=[10,-7,1]
        # initial_guess is a list containing the two starting values, e.g.-
        # initial_guess=[4,6]
        # max_iterations is the maximum allowed number of iterations
        # accuracy is the desired Accuracy
                
        self.co_eff=L
        self.initial_guess=initial_guess
        self.max_iterations=max_iterations
        self.accuracy=accuracy
        x,y=self.initial_guess[0],self.initial_guess[1]                     # x and y store the initial guesses
        count=0                                                             # variable to count number of iterations
        while(count<self.max_iterations):
            z=y-P.polyval(y,self.co_eff)*((y-x)/(P.polyval(y,self.co_eff)-P.polyval(x,self.co_eff))) # calculating next value
            if(abs(P.polyval(z,self.co_eff))<=self.accuracy):               # terminal condition
                return (z)                                                  # return value
            x,y=y,z                                                         # preparing for next iteration
            count+=1
        return ("Iterations exceed limit")

    def SecantRF(self,L,initial_guess,max_iterations,accuracy):             # SecantRF function takes 5 arguments
        
        # L is the co-efficient list, e.g.,-
        # If the polynomial is x^2-7x+10, then-
        # L=[10,-7,1]
        # initial_guess is a list containing the two starting values, e.g.-
        # initial_guess=[1,3]
        # max_iterations is the maximum allowed number of iterations
        # accuracy is the desired Accuracy
        
        self.co_eff=L
        self.initial_guess=initial_guess
        self.max_iterations=max_iterations
        self.accuracy=accuracy
        x,y=self.initial_guess[0],self.initial_guess[1]                     # x and y store the initial guesses
        count=0                                                             # variable to count number of iterations
        while(count<self.max_iterations):
            z=y-P.polyval(y,self.co_eff)*((y-x)/(P.polyval(y,self.co_eff)-P.polyval(x,self.co_eff))) # calculating next value
            if(abs(P.polyval(z,self.co_eff))<=self.accuracy):               # terminal condition
                return (z)                                                  # return value
            if(P.polyval(x,self.co_eff)*P.polyval(z,self.co_eff)>0):        # preparing for next iteration
                x,y=z,y
            else:
                x,y=x,z                                                     # preparing for next iteration
            count+=1
        return ("Iterations exceed limit")

    def NewtonRaphson(self,L,initial_guess,max_iterations,accuracy):        # NewtonRaphson function takes 5 arguments
        
        # L is the co-efficient list, e.g.,-
        # If the polynomial is x^2-9x+14, then-
        # L=[14,-9,1]
        # initial_guess is the starting value, e.g.-
        # initial_guess=8
        # max_iterations is the maximum allowed number of iterations
        # accuracy is the desired Accuracy
        
        self.co_eff=L
        self.initial_guess=initial_guess
        self.max_iterations=max_iterations
        self.accuracy=accuracy
        derivative=P.polyder(self.co_eff)                                   # calculating derivative of polynomial
        x=self.initial_guess                                                # variable to store initial guess
        count=0                                                             # variable to count iterations
        while(count<self.max_iterations):
            y=x-(P.polyval(x,self.co_eff)/P.polyval(x,derivative))          # calculating next value
            if(abs(P.polyval(y,self.co_eff))<=self.accuracy):               # terminal condition
                return (y)                                                  # return value
            x=y                                                             # preparing for next iteration
            count+=1
        return ("Iterations exceed limit")

ps=PolynomialSolver()                                                     # object creation
for method in ["bisection","secant","secantrf","newtonraphson"]:
    solution=ps.solve(2,[500,-60,1],[15,60],method,[1,15],1,10000,0.0001)
    print(solution)
