<template>
	<view class="lime-painter" ref="limepainter">
		<view v-if="canvasId && size" :style="size + customStyle">
			<!-- #ifndef APP-NVUE -->
			<canvas class="lime-painter__canvas" v-if="use2dCanvas" :id="canvasId" type="2d" :style="size"></canvas>
			<canvas class="lime-painter__canvas" v-else :canvas-id="canvasId" :style="size" :id="canvasId"
				:width="boardWidth * dpr" :height="boardHeight * dpr"></canvas>

			<!-- #endif -->
			<!-- #ifdef APP-NVUE -->
			<web-view v-if="hybrid || isInitFile" :style="size" ref="webview"
				:src="hybrid ? '/hybrid/html/lime-painter/index.html' :'_doc/uni_modules/lime-painter/index.html'"
				class="lime-painter__canvas" @pagefinish="onPageFinish" @error="onError" @onPostMessage="onMessage">
			</web-view>
			<!-- #endif -->
		</view>
		<slot />
	</view>
</template>


<script>
	import {
		parent
	} from '../common/relation'
	//  #ifndef APP-NVUE
	import {
		toPx,
		compareVersion,
		sleep,
		base64ToPath,
		pathToBase64,
		isBase64
	} from './utils';
	import Painter from './painter'
	//  #endif
	//  #ifdef APP-NVUE
	import {
		toPx,
		sleep,
		base64ToPath,
		pathToBase64,
		getImageInfo,
		isBase64,
		useNvue
	} from './utils';
	const dom = weex.requireModule('dom')
	import {
		version
	} from '../../package.json'
	//  #endif
	export default {
		name: 'lime-painter',
		mixins: [parent('painter')],
		props: {
			board: Object,
			pathType: String, // 'base64'、'url'
			fileType: {
				type: String,
				default: 'png'
			},
			quality: {
				type: Number,
				default: 1
			},
			css: [String, Object],
			width: [Number, String],
			height: [Number, String],
			pixelRatio: Number,
			customStyle: String,
			isCanvasToTempFilePath: Boolean,
			sleep: {
				type: Number,
				default: 1000 / 30
			},
			beforeDelay: {
				type: Number,
				default: 100
			},
			afterDelay: {
				type: Number,
				default: 100
			},
			// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
			type: {
				type: String,
				default: '2d'
			},
			// #endif
			// #ifdef APP-NVUE
			hybrid: Boolean,
			timeout: {
				type: Number,
				default: 2000
			}
			// #endif
		},
		data() {
			return {
				// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
				use2dCanvas: true,
				// #endif
				// #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
				use2dCanvas: false,
				// #endif
				canvasHeight: 150,
				canvasWidth: null,
				isPC: false,
				inited: false,
				progress: 0,
				// #ifdef APP-NVUE
				tempFilePath: [],
				isInitFile: false
				// #endif
			};
		},
		computed: {
			canvasId() {
				// #ifdef VUE3
				return `l-painter${this._.uid}`
				// #endif
				// #ifdef VUE2
				return `l-painter${this._uid}`
				// #endif
			},
			size() {
				if (this.boardWidth && this.boardHeight) {
					return `width:${this.boardWidth}px; height: ${this.boardHeight}px;`;
				}
			},
			dpr() {
				return this.pixelRatio || uni.getSystemInfoSync().pixelRatio;
			},
			boardWidth() {
				const {
					width = 0
				} = (this.board && this.board.css) || this.board || this
				return toPx(width) || Math.max(toPx(width), toPx(this.canvasWidth));
			},
			boardHeight() {
				const {
					height = 0
				} = (this.board && this.board.css) || this.board || this
				return toPx(height) || Math.max(toPx(height), toPx(this.canvasHeight));
			},
			elements() {
				return JSON.parse(JSON.stringify(this.el))
			}
		},
		watch: {
			canvasWidth(v) {
				if (this.el.css && !this.el.css.width) {
					this.el.css.width = v
				}
			},
			// #ifdef MP-WEIXIN ||  MP-ALIPAY
			size(v) {
				// #ifdef MP-WEIXIN
				if (this.use2dCanvas) {
					this.inited = false;
				}
				// #endif
				// #ifdef MP-ALIPAY
				this.inited = false;
				// #endif
			},
			// #endif
		},
		// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY 
		created() {
			const {
				SDKVersion,
				version,
				platform,
				environment
			} = uni.getSystemInfoSync();
			// #ifdef MP-WEIXIN
			this.isPC = /windows/i.test(platform)
			this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.9.2') >= 0 && !((/ios/i.test(
				platform) && /7.0.20/.test(version)) || /wxwork/i.test(environment)) && !this.isPC;
			// #endif
			// #ifdef MP-TOUTIAO
			this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '1.78.0') >= 0;
			// #endif
			// #ifdef MP-ALIPAY
			this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.7.0') >= 0;
			// #endif
		},
		// #endif
		// #ifdef APP-NVUE 
		created() {
			if (this.hybrid) return
			useNvue('_doc/uni_modules/lime-painter/', version, this.timeout).then(res => {
				this.isInitFile = true
			})
		},
		// #endif
		async mounted() {
			await this.getParentWeith()
			this.$nextTick(() => {
				setTimeout(() => {
					if (this.board) {
						this.$watch('board', this.watchRender, {
							deep: true,
							immediate: true
						});
					} else {
						this.$watch('elements', this.watchRender, {
							deep: true,
							immediate: true
						});
					}
				}, 30)
			})
		},
		methods: {
			async watchRender(val, old) {
				this.progress = 0
				if (!val || !val.views || !val.views.length || JSON.stringify(val) === '{}' || JSON.stringify(val) ==
					JSON.stringify(old)) return;
				clearTimeout(this.rendertimer)
				this.rendertimer = setTimeout(() => {
					this.render(val);
				}, this.beforeDelay)
			},
			async setFilePath(path, isEmit) {
				let filePath = path
				const {
					pathType
				} = this
				if (pathType == 'base64' && !isBase64(path)) {
					filePath = await pathToBase64(path)
				} else if (pathType == 'url' && isBase64(path)) {
					filePath = await base64ToPath(path)
				}
				if (isEmit) {
					this.$emit('success', filePath);
				}
				return filePath
			},
			async getSize(args) {
				if (!this.size) {
					const {
						width
					} = args.css || args
					const {
						height
					} = args.css || args
					if (width || height) {
						this.canvasWidth = width || this.canvasWidth
						this.canvasHeight = height || this.canvasHeight
						await sleep(30);
					} else {
						await this.getParentWeith()
					}
				}
			},
			canvasToTempFilePathSync(args) {
				this.$watch('progress', (v) => {
					if (v == 1) {
						this.canvasToTempFilePath(args)
					}
				}, {
					immediate: true
				})
			},
			// #ifdef APP-NVUE
			onPageFinish() {
				this.$refs.webview.evalJS(`init(${this.dpr})`)
			},
			onMessage(e) {
				const res = e.detail.data[0] || null;
				if (res.event) {
					if (res.event == 'inited') {
						this.inited = true
					}
					if (res.event == 'layoutChange') {
						const data = JSON.parse(res.data)
						this.canvasWidth = data.width;
						this.canvasHeight = data.height;
					}
					if (res.event == 'progressChange') {
						this.progress = res.data * 1
					}
					if (res.event == 'file') {
						this.tempFilePath.push(res.data)
						if (this.tempFilePath.length > 7) {
							this.tempFilePath.shift()
						}
						return
					}
					if (res.event == 'success') {
						if (res.data) {
							this.tempFilePath.push(res.data)
							if (this.tempFilePath.length > 8) {
								this.tempFilePath.shift()
							}
							if (this.isCanvasToTempFilePath) {
								this.setFilePath(this.tempFilePath.join(''), true)
							}
						} else {
							this.$emit('fail', 'canvas no data')
						}
						return
					}
					this.$emit(res.event, JSON.parse(res.data));
				} else if (res.file) {
					this.file = res.data;
				} else if (/ms$/.test(res[0])) {
					console.info(res[0])
				} else {
					this.$emit('fail', res)
				}
			},
			getWebViewInited() {
				if (this.inited) return Promise.resolve(this.inited);
				return new Promise((resolve) => {
					this.$watch(
						'inited',
						async val => {
							if (val) {
								resolve(val)
							}
						}, {
							immediate: true
						}
					);
				})
			},
			getTempFilePath() {
				if (this.tempFilePath.length == 8) return Promise.resolve(this.tempFilePath)
				return new Promise((resolve) => {
					this.$watch(
						'tempFilePath',
						async val => {
							if (val.length == 8) {
								resolve(val.join(''))
							}
						}
					);
				})
			},
			getWebViewDone() {
				if (this.progress == 1) return Promise.resolve(this.progress);
				return new Promise((resolve) => {
					this.$watch(
						'progress',
						async val => {
							if (val == 1) {
								this.$emit('done')
								resolve(val)
							}
						}, {
							immediate: true
						}
					);
				})
			},
			async render(args) {
				try {
					await this.getSize(args)
					const newNode = await this.calcImage(args);
					await this.getWebViewInited()
					const webview = this.$refs.webview;
					webview.evalJS(`source(${JSON.stringify(newNode)})`)
					await this.getWebViewDone()
					await sleep(this.afterDelay)
					if (this.isCanvasToTempFilePath) {
						const params = {
							fileType: this.fileType,
							quality: this.quality
						}
						webview.evalJS(`save(${JSON.stringify(params)})`)
					}
					return Promise.resolve()
				} catch (e) {
					this.$emit('fail', e)
				}
			},
			async calcImage(args) {
				let node = JSON.parse(JSON.stringify(args))
				const urlReg = /url\((.+)\)/
				const isBG = node.css.backgroundImage && urlReg.exec(node.css.backgroundImage)?. [1]
				const url = node.url || node.src || isBG
				if ((node.type === "image" || isBG) && url && !isBase64(url)) {
					const {
						path,
						type
					} = await getImageInfo(url)
					if (isBG) {
						node.css.backgroundImage = `url(${path})`
					} else {
						node.src = path
					}
				} else if (node.views && node.views.length) {
					for (let i = 0; i < node.views.length; i++) {
						node.views[i] = await this.calcImage(node.views[i])
					}
				}
				return node
			},
			async canvasToTempFilePath(args = {}) {
				if (!this.inited) {
					return this.$emit('fail', 'no init')
				}
				this.tempFilePath = []
				if (args.fileType == 'jpg') {
					args.fileType = 'jpeg'
				}
				this.$refs.webview.evalJS(`save(${JSON.stringify(args)})`)
				try {
					let tempFilePath = await this.getTempFilePath()
					tempFilePath = await this.setFilePath(tempFilePath)
					args.success({
						errMsg: "canvasToTempFilePath:ok",
						tempFilePath
					})
				} catch (e) {
					args.fail({
						error: e
					})
				}
			},
			// #endif
			getParentWeith() {
				return new Promise(resolve => {
					// #ifdef APP-NVUE
					dom.getComponentRect(this.$refs.limepainter, (res) => {
						this.canvasWidth = this.canvasWidth || Math.ceil(res.size.width)||300
						this.canvasHeight = res.size.height || this.canvasHeight||150
						resolve(res.size)
					})
					// #endif
					// #ifndef APP-NVUE
					uni.createSelectorQuery()
						.in(this)
						.select(`.lime-painter`)
						.boundingClientRect()
						.exec(res => {
							this.canvasWidth = Math.ceil(res[0].width)||300
							this.canvasHeight = res[0].height || this.canvasHeight||150
							resolve(res[0])
						})
					// #endif
				})
			},

			// #ifndef APP-NVUE
			async render(args = {}) {
				await this.getSize(args)
				const ctx = await this.getContext();
				let {
					use2dCanvas,
					boardWidth,
					boardHeight,
					canvas,
					afterDelay
				} = this;
				if (use2dCanvas && !canvas) {
					return Promise.reject(new Error('render: fail canvas has not been created'));
				}
				this.boundary = {
					top: 0,
					left: 0,
					width: boardWidth,
					height: boardHeight
				};
				if (!this.painter) {
					this.painter = new Painter({
						context: ctx,
						canvas,
						width: boardWidth,
						height: boardHeight,
						pixelRatio: this.dpr,
						fixed: `${this.width?'width':''}${this.height?'height':''}`,
						listen: {
							onProgress: (v) => {
								this.progress = v
								this.$emit('progress', v)
							},
							onEffectFail: (err) => {
								this.$emit('faill', err)
							}
						}
					}, this)
				}
				const {
					width,
					height
				} = await this.painter.source(args)
				this.boundary.height = this.canvasHeight = height
				this.boundary.width = this.canvasWidth = width
				await sleep(this.sleep);
				await this.painter.render()
				await new Promise(resolve => this.$nextTick(resolve));
				if (!use2dCanvas) {
					await this.canvasDraw();
				}
				if (afterDelay && use2dCanvas) {
					await sleep(afterDelay);
				}
				this.$emit('done');
				if (this.isCanvasToTempFilePath) {
					this.canvasToTempFilePath()
						.then(async res => {
							this.$emit('success', res.tempFilePath)
						})
						.catch(err => {
							this.$emit('fail', new Error(JSON.stringify(err)));
						});
				}
				return Promise.resolve({
					ctx,
					draw: this.painter,
					node: this.node
				});
			},
			canvasDraw(flag = false) {
				return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this
					.afterDelay)));
			},
			async getContext() {
				if (!this.canvasWidth) {
					this.$emit('fail', 'painter no size')
					console.error('painter no size: 请给画板或父级设置尺寸')
					return Promise.reject();
				}
				if (this.ctx && this.inited) {
					return Promise.resolve(this.ctx);
				}
				const {
					type,
					use2dCanvas,
					dpr,
					boardWidth,
					boardHeight
				} = this;
				const _getContext = () => {
					return new Promise(resolve => {
						uni.createSelectorQuery()
							.in(this)
							.select(`#${this.canvasId}`)
							.boundingClientRect()
							.exec(res => {
								if (res) {
									const ctx = uni.createCanvasContext(this.canvasId, this);
									if (!this.inited) {
										this.inited = true;
										this.use2dCanvas = false;
										this.canvas = res;
									}
									if (this.isPC) {
										ctx.scale(1 / dpr, 1 / dpr);
									}
									// #ifdef MP-ALIPAY
									ctx.scale(dpr, dpr);
									// #endif
									this.ctx = ctx
									resolve(this.ctx);
								}
							});
					});
				};
				// #ifndef MP-WEIXIN
				return _getContext();
				// #endif

				if (!use2dCanvas) {
					return _getContext();
				}
				return new Promise(resolve => {
					uni.createSelectorQuery()
						.in(this)
						.select(`#${this.canvasId}`)
						.node()
						.exec(res => {
							let {
								node: canvas
							} = res[0];
							if (!canvas) {
								this.use2dCanvas = false;
								resolve(this.getContext());
							}
							const ctx = canvas.getContext(type);
							if (!this.inited) {
								this.inited = true;
								this.use2dCanvas = true;
								this.canvas = canvas;
							}
							this.ctx = ctx
							resolve(this.ctx);
						});
				});
			},
			canvasToTempFilePath(args = {}) {
				const {
					use2dCanvas,
					canvasId,
					dpr,
					fileType,
					quality
				} = this;
				return new Promise((resolve, reject) => {
					let {
						top: y = 0,
						left: x = 0,
						width,
						height
					} = this.boundary || this;
					let destWidth = width * dpr;
					let destHeight = height * dpr;
					// #ifdef MP-ALIPAY
					width = destWidth;
					height = destHeight;
					// #endif
					const success = async (res) => {
						try {
							const tempFilePath = await this.setFilePath(res.tempFilePath)
							resolve(Object.assign(res, {
								tempFilePath
							}))
						} catch (e) {
							this.$emit('fail', e)
						}
					}
					const copyArgs = Object.assign({
						x,
						y,
						width,
						height,
						destWidth,
						destHeight,
						canvasId,
						fileType,
						quality,
						success,
						fail: reject
					}, args);
					if (use2dCanvas) {
						delete copyArgs.canvasId;
						copyArgs.canvas = this.canvas;
					}
					uni.canvasToTempFilePath(copyArgs, this);
				});
			}
			// #endif
		}
	};
</script>
<style>
	.lime-painter,
	.lime-painter__canvas {
		// #ifndef APP-NVUE
		width: 100%;
		// #endif
		// #ifdef APP-NVUE
		flex: 1;
		// #endif
	}
</style>
