'use strict'

const Canvas=require('./canvas')

class PhaseCanvas extends Canvas {
	redraw(matrix,xLabel,yLabel) {
		const ctx=this.ctx
		const xRange=this.width/2
		const yRange=this.height/2
		const drawDirectionField=()=>{
			const arrowSpacing=27
			const arrowLength=5
			const arrowWidth=2
			const drawArrow=(x,y)=>{
				const dx=matrix.a*x+matrix.b*y
				const dy=matrix.c*x+matrix.d*y
				const dl=Math.sqrt(dx*dx+dy*dy)
				ctx.beginPath()
				ctx.moveTo(
					+(x-dx/dl*arrowLength),
					-(y-dy/dl*arrowLength)
				)
				ctx.lineTo(+x,-y)
				ctx.stroke()
				ctx.beginPath()
				ctx.moveTo(
					+(x+dx/dl*arrowLength),
					-(y+dy/dl*arrowLength)
				)
				ctx.lineTo(
					+(x-dy/dl*arrowWidth),
					-(y+dx/dl*arrowWidth)
				)
				ctx.lineTo(
					+(x+dy/dl*arrowWidth),
					-(y-dx/dl*arrowWidth)
				)
				ctx.closePath()
				ctx.fill()
			}
			ctx.save()
			ctx.fillStyle=ctx.strokeStyle='#888'
			const nx=Math.floor(xRange/arrowSpacing)
			const ny=Math.floor(yRange/arrowSpacing)
			for (let i=-nx;i<=nx;i++) {
				for (let j=-ny;j<=ny;j++) {
					drawArrow(
						i*arrowSpacing+0.5*Math.sign(i),
						j*arrowSpacing+0.5*Math.sign(j)
					)
				}
			}
			ctx.restore()
		}
		const drawEigenline=(lambda,otherLambda)=>{
			const iconSize=5
			let icon
			if (lambda<0) {
				icon=-1-(otherLambda<0 && lambda<otherLambda)
			} else if (lambda>0) {
				icon=+1+(otherLambda>0 && lambda>otherLambda)
			} else {
				icon=0
			}
			const xy=matrix.getEigenvector(lambda)
			let x=xy[0], y=xy[1]
			ctx.save()
			ctx.lineWidth=2
			if (x<0) {
				x=-x
				y=-y
			}
			if (y<0) {
				ctx.scale(1,-1)
				y=-y
			}
			if (x<y) { // TODO this will work only for square canvases, should also swap xRange and yRange
				ctx.transform(0,1,1,0,0,0) // swap x,y
				;[x,y]=[y,x]
			}
			if (x>0) {
				const r=Math.sqrt(x*x+y*y)
				const x0=x/r
				const y0=y/r
				const x1=xRange
				const y1=yRange*y/x
				const xic=xRange/2*x0
				const yic=yRange/2*y0
				const xis=iconSize*x0
				const yis=iconSize*y0
				const drawEigenlineIcon=()=>{
					const dir=Math.sign(icon)
					const drawDiamond=()=>{
						ctx.beginPath()
						ctx.moveTo(xic-xis,-(yic-yis))
						ctx.lineTo(xic+yis,-(yic-xis))
						ctx.lineTo(xic+xis,-(yic+yis))
						ctx.lineTo(xic-yis,-(yic+xis))
						ctx.closePath()
						ctx.fill()
					}
					const drawTriangle=(ofs)=>{
						ctx.beginPath()
						ctx.moveTo(xic+dir*(ofs*xis-xis-yis),-(yic+dir*(ofs*yis-yis+xis)))
						ctx.lineTo(xic+dir*(ofs*xis-xis+yis),-(yic+dir*(ofs*yis-yis-xis)))
						ctx.lineTo(xic+dir*(ofs*xis+xis    ),-(yic+dir*(ofs*yis+yis    )))
						ctx.closePath()
						ctx.fill()
					}
					if (dir==0) {
						drawDiamond()
					} else {
						drawTriangle(0)
						if (Math.abs(icon)>=2) {
							drawTriangle(2)
						}
					}
				}
				ctx.beginPath()
				ctx.moveTo(-x1,+y1)
				ctx.lineTo(+x1,-y1)
				ctx.stroke()
				ctx.save()
				drawEigenlineIcon()
				ctx.scale(-1,-1)
				drawEigenlineIcon()
				ctx.restore()
			}
			ctx.restore()
		}
		const drawSolution=(x0,y0,color)=>{
			const iterationLimit=100*Math.max(xRange,yRange)
			const fnOnFractions=(fn,xNum,xDen,yNum,yDen)=>{
				if (xDen!=0 && yDen!=0) {
					return fn(xNum/xDen,yNum/yDen)
				} else if (xDen==0 && yDen!=0) {
					return yNum/yDen
				} else if (xDen!=0 && yDen==0) {
					return xNum/xDen
				}
			}
			const drawFixedPoint=()=>{
				const fixedPointSize=3
				ctx.beginPath()
				ctx.arc(x0,y0,fixedPointSize,0,2*Math.PI)
				ctx.fill()
			}
			const drawSaddleOrNodeSolution=()=>{
				let lambda1,lambda2
				if (matrix.re1<=matrix.re2) {
					lambda1=matrix.re1
					lambda2=matrix.re2
				} else {
					lambda1=matrix.re2
					lambda2=matrix.re1
				}
				const xy1=matrix.getEigenvector(lambda1)
				let x1=xy1[0], y1=xy1[1]
				const xy2=matrix.getEigenvector(lambda2)
				let x2=xy2[0], y2=xy2[1]
				if (lambda2<=0) {
					;[lambda1,lambda2]=[-lambda2,-lambda1]
					;[x1,x2]=[x2,x1]
					;[y1,y2]=[y2,y1]
				}
				let k1=+(x2*y0-x0*y2)/(x2*y1-x1*y2)
				let k2=-(x1*y0-x0*y1)/(x2*y1-x1*y2)
				if ((k1==0 && k2==0) || (lambda1==0 && k2==0)) {
					drawFixedPoint()
					return
				} else if (k1==0 && lambda2!=0) {
					lambda1=0 // trigger comb case below
				} else if (k2==0 && lambda1!=0) {
					lambda2=Math.abs(lambda1)
					lambda1=0 // trigger comb case below
					;[x1,x2]=[x2,x1]
					;[y1,y2]=[y2,y1]
					;[k1,k2]=[k2,k1]
				}
				if (lambda1>0) { // node
					let T1=0
					let T2=0
					const dT1=1/Math.max(Math.abs(k1*x1),Math.abs(k1*y1)) // step size is controlled by slow direction
					ctx.beginPath()
					ctx.moveTo(0,0)
					for (let i=0;i<iterationLimit;i++) {
						T1+=dT1
						T2=Math.pow(T1,lambda2/lambda1)
						ctx.lineTo(
							+(k1*x1*T1+k2*x2*T2),
							-(k1*y1*T1+k2*y2*T2)
						)
						if (
							(k1*x1*T1+k2*x2*T2)*Math.sign(k2*x2)>xRange ||
							(k1*y1*T1+k2*y2*T2)*Math.sign(k2*y2)>yRange
						) break
					}
					ctx.stroke()
				} else if (lambda1<0 && lambda2>0) { // saddle
					let t=0 // assumes starting point is in viewport
					for (let i=0;i<iterationLimit;i++) {
						const ke1=k1*Math.exp(lambda1*t)
						const ke2=k2*Math.exp(lambda2*t)
						if (
							(x1*ke1+x2*ke2)*Math.sign(x1*k1)>xRange ||
							(y1*ke1+y2*ke2)*Math.sign(y1*k1)>yRange
						) break
						const dt=Math.min(
							1/Math.abs(x1*ke1*lambda1),
							1/Math.abs(y1*ke1*lambda1),
							1/Math.abs(x1*ke2*lambda2),
							1/Math.abs(y1*ke2*lambda2)
						)
						t-=dt
					}
					ctx.beginPath()
					for (let i=0;i<iterationLimit;i++) {
						const ke1=k1*Math.exp(lambda1*t)
						const ke2=k2*Math.exp(lambda2*t)
						ctx[i?'lineTo':'moveTo'](
							+(x1*ke1+x2*ke2),
							-(y1*ke1+y2*ke2)
						)
						if (t>0 && (
							(x1*ke1+x2*ke2)*Math.sign(x1*k2)>xRange ||
							(y1*ke1+y2*ke2)*Math.sign(y1*k2)>yRange
						)) break
						const dt=Math.min(
							1/Math.abs(x1*ke1*lambda1),
							1/Math.abs(y1*ke1*lambda1),
							1/Math.abs(x1*ke2*lambda2),
							1/Math.abs(y1*ke2*lambda2)
						)
						t+=dt
					}
					ctx.stroke()
				} else if (lambda1==0 && lambda2>0) { // comb
					const t=fnOnFractions(Math.min,
						Math.sign(x2*k2)*xRange-x1*k1, x2*k2,
						Math.sign(y2*k2)*yRange-y1*k1, y2*k2
					)
					if (t>0) {
						ctx.beginPath()
						ctx.moveTo(
							+(x1*k1),
							-(y1*k1)
						)
						ctx.lineTo(
							+(x1*k1+x2*k2*t),
							-(y1*k1+y2*k2*t)
						)
						ctx.stroke()
					}
				}
			}
			const drawSpiralSolution=()=>{
				let alpha=matrix.re1
				let beta=matrix.im1
				if (beta<0) {
					beta=-beta
				}
				const xxyy=matrix.getComplexEigenvector(alpha,beta)
				if (alpha<0) {
					alpha=-alpha
					beta=-beta
				}
				const x1=xxyy[0], x2=xxyy[1]
				const y1=xxyy[2], y2=xxyy[3]
				const k1=+(x2*y0-x0*y2)/(x2*y1-x1*y2)
				const k2=-(x1*y0-x0*y1)/(x2*y1-x1*y2)
				if (k1==0 && k2==0) return
				const kxc=k1*x1+k2*x2
				const kxs=k2*x1-k1*x2
				const kyc=k1*y1+k2*y2
				const kys=k2*y1-k1*y2
				if (alpha==0) { // center
					ctx.beginPath()
					const dt=1/Math.max(kxc,kyc,kxs,kys)
					let t=0
					for (let i=0;i<iterationLimit;i++) {
						const c=Math.cos(t)
						const s=Math.sin(t)
						ctx[i?'lineTo':'moveTo'](
							+(kxc*c+kxs*s),
							-(kyc*c+kys*s)
						)
						t+=dt
						if (t>=2*Math.PI) break
					}
					ctx.closePath()
					ctx.stroke()
				} else { // spiral
					const calculateSemiaxes=()=>{ // https://en.wikipedia.org/wiki/Ellipse#Canonical_form
						const A=kys*kys+kyc*kyc, B=-2*(kxs*kys+kxc*kyc), C=kxs*kxs+kxc*kxc
						const f=kxc*kys-kxs*kyc
						const F=-f*f
						const DIS=B*B-4*A*C
						const A2=2*DIS*F*(A+C-Math.sqrt((A-C)*(A-C)+B*B))/(DIS*DIS)
						const B2=2*DIS*F*(A+C+Math.sqrt((A-C)*(A-C)+B*B))/(DIS*DIS)
						return [A2,B2] // TODO relative axes lengths don't depend on initial conditions, this calculation should be simpler
					}
					const A2B2=calculateSemiaxes()
					const A2=A2B2[0]
					const B2=A2B2[1]
					const B=Math.sqrt(B2)
					const R2=xRange*xRange+yRange*yRange
					//const t0=-(Math.log(B2)/2+Math.log(Math.exp(2*Math.PI*alpha/Math.abs(beta))-1))/alpha
					const lineWidth=2
					const t0=(Math.log(lineWidth)-Math.log(B2)/2-Math.log(Math.exp(2*Math.PI*alpha/Math.abs(beta))-1))/alpha
					//const t1=(Math.log(R2)-Math.log(A2))/(2*alpha)
					ctx.beginPath()
					const e0=Math.exp(alpha*t0)
					const nSegments=Math.ceil(B*e0*2*Math.PI)
					for (let i=0;i<nSegments;i++) {
						const t=2*Math.PI*i/nSegments
						const c0=Math.cos(t)
						const s0=Math.sin(t)
						ctx[i?'lineTo':'moveTo'](
							+e0*(kxc*c0+kxs*s0),
							-e0*(kyc*c0+kys*s0)
						)
					}
					ctx.closePath()
					ctx.stroke()
					ctx.fill()
					ctx.beginPath()
					let t=t0
					for (let i=0;i<iterationLimit;i++) {
						const e=Math.exp(alpha*t)
						const c=Math.cos(beta*t)
						const s=Math.sin(beta*t)
						ctx[i?'lineTo':'moveTo'](
							+e*(kxc*c+kxs*s),
							-e*(kyc*c+kys*s)
						)
						//const dtTan=1/Math.abs(beta*B*e)
						const dtRad=Math.log(1/(B*e)+1)/alpha
						//t+=Math.min(Math.max(dtTan,1/Math.abs(40*beta)),dtRad)
						t+=Math.min(dtRad,1/Math.abs(16*beta))
						if (e*e*A2>R2) break
					}
					ctx.stroke()
				}
			}
			const drawDefectiveSolution=()=>{
				let lambda=matrix.re1
				let x1=(matrix.a-lambda)*x0+matrix.b*y0
				let y1=matrix.c*x0+(matrix.d-lambda)*y0
				if (lambda<0) {
					lambda=-lambda
					x1=-x1
					y1=-y1
				}
				if (lambda==0 && x1==0 && y1==0) { // everywhere fixed
					drawFixedPoint()
				} if (lambda==0) { // parallel lines
					const t0=fnOnFractions(Math.min,
						-Math.sign(x1)*xRange-x0, x1,
						-Math.sign(y1)*yRange-y0, y1
					)
					const t1=fnOnFractions(Math.max,
						+Math.sign(x1)*xRange-x0, x1,
						+Math.sign(y1)*yRange-y0, y1
					)
					ctx.beginPath()
					ctx.moveTo(
						+(x0+t0*x1),
						-(y0+t0*y1)
					)
					ctx.lineTo(
						+(x0+t1*x1),
						-(y0+t1*y1)
					)
					ctx.stroke()
				} else { // star or defective node
					ctx.beginPath()
					ctx.moveTo(0,0)
					let T=0
					const dT=1/Math.max(Math.abs(x0),Math.abs(y0))
					for (let i=0;i<iterationLimit;i++) {
						T+=dT
						const t=Math.log(T)/lambda
						ctx.lineTo(
							+T*(x0+t*x1),
							-T*(y0+t*y1)
						)
						if (
							T*(x0+t*x1)*Math.sign(x0)>xRange ||
							T*(y0+t*y1)*Math.sign(y0)>yRange
						) break
					}
					ctx.stroke()
				}
			}
			ctx.save()
			ctx.lineWidth=2
			ctx.strokeStyle=ctx.fillStyle=color
			if (matrix.im1==0 && matrix.re1!=matrix.re2) {
				drawSaddleOrNodeSolution()
			} else if (matrix.im1==0 && matrix.re1==matrix.re2) {
				drawDefectiveSolution()
			} else if (matrix.im1!=0) {
				drawSpiralSolution()
			}
			ctx.restore()
		}
		ctx.save()
		ctx.fillStyle='#FFF'
		ctx.fillRect(0,0,this.width,this.height)
		ctx.restore()
		ctx.save()
		ctx.translate(xRange,yRange)
		this.drawAxes(xLabel,yLabel)
		drawDirectionField()
		if (matrix.im1==0) {
			drawEigenline(matrix.re1,matrix.re2)
			if (matrix.re1!=matrix.re2) {
				drawEigenline(matrix.re2,matrix.re1)
			}
		}
		drawSolution(+xRange/2,0,'#08F')
		drawSolution(-xRange/2,0,'#28F')
		drawSolution(0,+yRange/2,'#0AF')
		drawSolution(0,-yRange/2,'#08D')
		ctx.restore()
	}
}

module.exports=PhaseCanvas
