import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
import matplotlib.patches as pat
import matplotlib.animation as anima
import math

class Coordinate():

    def __init__(self, init_x, init_y, ctp, rad, in_out, dx, dy):
        # ボールの初期位置を設定している。
        self.ball_x = init_x
        self.ball_y = init_y

        # line_y とは、ボールが領域内にあるかどうかの判断として使う変数である。
        ## line_y[i] > 0 ならば ボールは円の外側にあり、
        ## line_y[i] < 0 ならば ボールは円の内側にある。
        self.line_y = []

        # dx dy はボールが1回の描画で動く増加量である。
        self.dx = dx
        self.dy = dy

        # dx==0 ならば、ボールはx軸に対して垂直に動いている。
        ## 傾きが無限大のときは、infinity1=-99999という整数値とした。
        ## vertical_dy はボールが垂直で動くとき、1回で動く量とした。
        ## infinity1 と vertical_dy は main の中でGlobal 変数として定義している。
        if self.dx != 0:
            self.old_incli = self.dy/self.dx
        else:
            self.old_incli = infinity1
            self.dx=0
            self.dy = vertical_dy
   
    def state_update(self):
        # ボールの次の位置を計算している。
        ## ball_x、ball_y がボールの x座標と y座標を表している。
        ## ボールが進む直線の傾きをnew_incli としている。
        if self.dx != 0:
            new_incli = self.dy/self.dx
        else:
            new_incli = infinity1
            self.dx=0
            self.dy = vertical_dy
        
        # divider はボールの動く量を調整する変数であり、
        ## 直線の傾きが大きいほど、dx の増加量に対してdy の増加量が極端に大きく
        ## なるので、その調整としてdividerを考えた。
        divider = 2
 
        # dd[i]は円の中心とボールとの距離と、その円の半径との差を表す変数である。
        dd=[]
        # distanceは円の中心とボールとの距離と、その円の半径との差を求める関数である。
        for k in range(3):
            dd.append(abs(distance(ctp[k][0],ctp[k][1],rad[k],self.ball_x,self.ball_y)))

        # 直線に近づくほど、増加量を小さくし、ボールの速度を遅くする。     
        if min(dd) <= 0.4:
            divider +=5
        elif min(dd) <= 0.3:
            divider +=6
        elif min(dd) <= 0.2:
            divider +=7
        elif min(dd) <= 0.15:
            divider +=10
        elif min(dd) <= 0.1:
            divider +=12
        elif min(dd) <= 0.05:
            divider +=14
        elif min(dd) <= 0.01:
            divider +=18

        # 次に進むボールのx座標及びy座標を計算する。
        self.ball_x += self.dx/divider
        self.ball_y += self.dy/divider

        # line_y[k] は円の中心とボールとの距離と円の半径との差の値である。
        ## ボールが、円の外側のときは+、内側のときは-になる。
        self.line_y.clear()
        for k in range(3):
            self.line_y.append(distance(ctp[k][0],ctp[k][1],rad[k],self.ball_x,self.ball_y))
        
        # ボールが領域内にあるかどうかの判断をしている。
        ## line_side[j] には、領域の指定が main で定義されているが、
        ## 領域が境界線の上側は1、下側は-1 と定義されているので、
        ## in_out[j] との積をとって、それが正になったら領域から外れている。
        for j in range(3):
            if self.line_y[j] * in_out[j] > 0:

                # ボールが領域から外れたので、元に戻す。
                self.ball_x -= self.dx/divider
                self.ball_y -= self.dy/divider

                # old_incli が跳ね返る前の進入するボールの直線の傾きである。
                self.old_incli = new_incli

                # 円の接線の傾きを求める。
                m=tanj(self.ball_x,self.ball_y,ctp[j][0],ctp[j][1])

                # 入射角と反射角が等しいことから、跳ね返る直線の傾きを求めている。               
                new_incli = incli_line(m,self.old_incli)

                # 跳ね返った後のボールの増加量dx、dy を求めている。
                self.dx,self.dy=define_dx_dy(self.dx,self.dy,m,new_incli,self.old_incli)
   
                break
     
        return self.ball_x, self.ball_y


class Drawing():
    def __init__(self, ax, marks):
        self.ax = ax
        self.marks = marks
        self.ball_img, = ax.plot([], [], color="red")

    def draw_circle(self, center_x, center_y, circle_size=0.02):
        # ボールを描くメソッド
        ## marks == True のとき、matplotlib のCircleメソッドを使って描画し、
        ## marks == False のとき、媒介変数θを使った円の方程式 x=rcosθ、y=rsinθを使って描画する。
        self.circle_x = []
        self.circle_y = []

        if self.marks:
            c=pat.Circle((center_x,center_y),radius=circle_size,fc='blue',ec='red')
            ax.add_patch(c)
        else:
            steps = 20
            for j in range(steps):
                self.circle_x.append(center_x+circle_size*math.cos(j*2*math.pi/steps))
                self.circle_y.append(center_y+circle_size*math.sin(j*2*math.pi/steps))

        self.ball_img.set_data(self.circle_x, self.circle_y)

        return self.ball_img,

def define_dx_dy(dx,dy,m,new_incli,old_incli):
    # 跳ね返ったときの x,y の増分 dx,dy を計算する関数である。
    ## 渡された dx,dy は跳ね返る前の増分なので、まず、x,yの増分を old_dx,old_dyとして保存する。
    old_dy = dy
    old_dx = dx

    # new_incli == infinity1 の場合は、跳ね返ったボールは、x軸に垂直に動くので、x の増分は0、
    ## y の増分は上に向かって跳ね返ったときは負になり、下に向かって跳ね返ったときは正になる。
    if new_incli == infinity1:
        dx = 0
        if old_dy > 0:
            dy = -vertical_dy
        elif old_dy < 0:
            dy = vertical_dy
    ## y の増分が0、すなわちボールがx軸に平行に動くときは、x の増分が正 かつ 境界線の傾きが正のとき、または、
    ## x の増分が負 かつ 境界線の傾きが負のときは、ボールは上に向かう。つまり、y の増分は正になる。
    ## 逆に、x の増分が正 かつ 境界線の傾きが負のとき、または、x の増分が負 かつ 境界線の傾きが正のとき、
    ## ボールは下に向かう。つまり、y の増分は負になる。これを、x の増分と境界線の傾きの積の符号によって判断する。        
        elif old_dy == 0:
            if old_dx * m > 0:
                dy = vertical_dy
            else:
                dy = -vertical_dy

    else:
    # y の増分は跳ね返った直線の傾きに、x の増分を掛ける。 
        dy = new_incli * incriment_x

        # 進入するボールはベクトルのイメージで(old_dx,old_dy)が方向ベクトルになる。
        ## 跳ね返った後のボールの傾きはnew_incli であるが、どちらに進んでいいのかの判断が、
        ## 煩雑であり、下にある条件文である。一つ一つのコメントは割愛する。
        if old_dx > 0 and old_dy >= 0 and new_incli >= 0:
            if m >= 0:
                dx = abs(dx)
                dy = abs(dy)
            else:
                dx = -abs(dx)
                dy = -abs(dy)

        elif old_dx > 0 and old_dy >= 0 and new_incli < 0:
            if m >= 0:
                dx = -abs(dx)
                dy = abs(dy)
            else:
                if m >= new_incli:
                    dx = abs(dx)
                    dy = -abs(dy)
                else:
                    dx = -abs(dx)
                    dy = abs(dy)

        elif old_dx > 0 and old_dy < 0 and new_incli >= 0:
            if m >= 0:
                if m >= new_incli:
                    dx = -abs(dx)
                    dy = -abs(dy)
                else:
                    dx = abs(dx)
                    dy = abs(dy)
            else:
                if m >= old_incli:
                    dx = abs(dx)
                    dy = abs(dy)
                else:
                    dx = -abs(dx)
                    dy = -abs(dy)

        elif old_dx < 0 and old_dy >= 0 and new_incli >= 0:
            if m >= 0:
                if m >= new_incli:
                    dx = abs(dx)
                    dy = abs(dy)
                else:
                    dx = -abs(dx)
                    dy = -abs(dy)
            else:
                if m >= old_incli:
                    dx = -abs(dx)
                    dy = -abs(dy)
                else:
                    dx = abs(dx)
                    dy = abs(dy)

        elif old_dx > 0 and old_dy < 0 and new_incli < 0:
            if m >= 0:
                dx = -abs(dx)
                dy = abs(dy)
            else:
                if m >= old_incli:
                    dx = abs(dx)
                    dy = -abs(dy)
                else:
                    dx = abs(dx)
                    dy = -abs(dy)

        elif old_dx < 0 and old_dy >= 0 and new_incli < 0:
            if m >= 0:
                dx = abs(dx)
                dy = -abs(dy)
            else:
                if m >= old_incli:
                    dx = -abs(dx)
                    dy = -abs(dy)
                else:
                    dx = abs(dx)
                    dy = -abs(dy)

        elif old_dx < 0 and old_dy < 0 and new_incli >= 0:
            if m >= 0:
                dx = -abs(dx)
                dy = -abs(dy)
            else:
                dx = abs(dx)
                dy = abs(dy)
                
        elif old_dx < 0 and old_dy < 0 and new_incli < 0:
            if m >= 0:
                if m >= old_incli:
                    dx = abs(dx)
                    dy = -abs(dy)
                else:
                    dx = -abs(dx)
                    dy = abs(dy)
            else:
                if m >= new_incli:
                    dx = -abs(dx)
                    dy = abs(dy)
                else:
                    dx = abs(dx)
                    dy = -abs(dy)

    return dx,dy

def update_an(i):
    # FuncAnimation に渡すための関数
    ball_x, ball_y= ball.state_update()
    ball_img, = drawer.draw_circle(ball_x, ball_y)

    return ball_img, 

def tanj(x,y,a,b):
    if (y-b) != 0:
        m=-(x-a)/(y-b)
    else:
        m=infinity1
    return m


def incli_line(m,incli):
    # 跳ね返った直線の傾きを計算する関数
    ## 入射角と反射角が等しいといことから、導かれる。
    ## m が境界線の傾きで、incli が入射する直線の傾きである。
    incli_limit = 1000

    if m == infinity1:
        new_incli = -incli
    else:
        if incli == infinity1:
            new_incli = (1-m**2)/2*m
        else:
            denomi = 1+2*m*incli-m**2
            if denomi != 0:
                new_incli = (2*m+incli*m**2-incli)/denomi
            else:
                new_incli = infinity1

            if new_incli < -incli_limit or new_incli > incli_limit:
                new_incli = infinity1
    
    return new_incli

def distance(a,b,rad,x,y):
    # ボールと境界線までの距離を計算する関数
    dd = math.sqrt((x-a)**2+(y-b)**2)-rad

    return dd

####################################################################
#########################__MAIN__###################################
####################################################################
# infinity1=-99999 は、跳ね返った直線がx軸に対して垂直なったとき
## の傾きの値として使う。
infinity1=-99999
# 画面の初期設定で、この詳細はOh!Pythonの第1版を参照。
fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title("Oh!PYTHON Mathmatics 2", fontsize = 16)
# 境界線を設定する。問題は3つの円で囲まれた領域をボールが跳ねるので、
## その3つの境界線の設定をここで行う。
## 3境界線の設定 ctp=(円1の中心,円2の中心,円3の中心) 
##               rad=(円1の半径,円2の半径,円3の半径)
ctp=((-1,-1),(1,1),(-1,-2))
rad=(4,1,2)
# 領域の指定をする。 円の内側は1、円の外側は-1 とする。
## さらに、in_out = (円1の内外,円2の内外,円3の内外) とする。
## この例は、円1は内側、円2は外側、円3はは外側になる。
in_out=(1,-1,-1)

# グラフの範囲は、中心の座標に各半径を±して計算する。
max_x=max(ctp[0][0]+rad[0],ctp[1][0]+rad[1],ctp[2][0]+rad[2])
min_x=min(ctp[0][0]-rad[0],ctp[1][0]-rad[1],ctp[2][0]-rad[2])
max_y=max(ctp[0][1]+rad[0],ctp[1][1]+rad[1],ctp[2][1]+rad[2])
min_y=min(ctp[0][1]-rad[0],ctp[1][1]+rad[1],ctp[2][1]+rad[2])
# グラフの範囲は上で求めた最小値に-2、最大値に+2 をして多少広げる。
max_x_Axis = int(max_x)+1
min_x_Axis = int(min_x)-1
max_y_Axis = int(max_y)+1
min_y_Axis = int(min_y)-1
# グラフの範囲の設定をする。
ax.set_xlim(min_x_Axis, max_x_Axis)
ax.set_ylim(min_y_Axis, max_y_Axis)
# x軸、y軸をannotate を使って表示する。
ax.annotate("", xy = (max_x_Axis, 0), xytext = (min_x_Axis, 0),
             arrowprops = dict(shrink=0, width=0.1, headwidth=4, 
                                headlength=5, connectionstyle='arc3',
                                facecolor='k', edgecolor='k'))
ax.annotate("", xy = (0,max_y_Axis), xytext = (0,min_y_Axis),
             arrowprops = dict(shrink=0, width=0.1, headwidth=4, 
                                headlength=5, connectionstyle='arc3',
                                facecolor='k', edgecolor='k'))
# x,y軸及び原点Oに名前を設定する。
ax.text(max_x_Axis-0.4,-0.4,'x',fontsize=10)
ax.text(-0.4,max_y_Axis-0.4,'y',fontsize=10)
ax.text(-0.4,-0.4,'O',fontsize=10)
# 座標平面にグリッドを青色で設定する。
ax.grid(which="major",axis="x",color="blue",alpha=0.8,linestyle="--",linewidth=1)
ax.grid(which="major",axis="y",color="blue",alpha=0.8,linestyle="--",linewidth=1)
# 座標平面の縦と横の比を等しくする
ax.set_aspect('equal')
# x,y の増加量の初期値を設定する。
## incriment_x は x の基本の増加量,inciment_y は 最初のボールがどちらに向かうかの量
## すなわち、incriment_y/incriment_x の値は、最初のボールの向かう直線の傾きを表す
## vertival_dy は x軸について垂直になったときの増加量
incriment_x = 0.2
incriment_y = 0.3
vertical_dy = 0.001
# 2つの境界線を描画する。
for i in range(3):
    cir=pat.Circle(xy=ctp[i],radius=rad[i],fill=False,ec='k')
    ax.add_patch(cir)
# ボールの位置を計算するインスタンスを作成する。
## Coordinate(xの初期値,yの初期値,円の中心,円の半径,領域,x の増加量,y の増加量)

ball = Coordinate(-2, 1, ctp, rad, in_out,incriment_x,incriment_y)
# ballで計算した位置に円を描画するインスタンスを作成する。
## marks は軌跡の設定で、 True=足跡を付ける False=足跡を付けない となる。
marks = False
drawer = Drawing(ax,marks)
# アニメーションのライブラリに渡す
animation = anima.FuncAnimation(fig, update_an, interval=5, frames=10)

plt.show()