<div id="root"></div>
const { useState, useRef, useCallback, useEffect, useReducer } = React;
const { createGlobalStyle, css } = styled;

const SLIDE_PUZZLE_SIZE = {
	height: "60vmin",
	width: "60vmin"
};

const BG =
	"https://images.ctfassets.net/w6gireivdm0c/61UEuQEwHoSoz3GIpNkF9e/1d18ada4da6c6de4912efa71f57d44aa/NEKO9V9A9131_TP_V4.jpg";
const SHUFFLE_SPEED = 60;
const MAX_SHUFFLE_COUNT = 100;
const LEVEL = {
	easy: 9,
	normal: 16,
	hard: 25
};
const BOARD_COLOR = "#997f5d";

const fitImage = async (url, h, w) => {
	const image = await loadImage(url).then((r) => r);

	const width = image.width;
	const height = image.height;

	const ratio = height / width;

	const fittedImageSize = {
		height: 0,
		width: 0
	};

	if (ratio >= 1) {
		fittedImageSize.height = height * (w / width);
		fittedImageSize.width = w;
	} else {
		fittedImageSize.height = h;
		fittedImageSize.width = width * (h / height);
	}

	return fittedImageSize;
};

const clamp = (value, min, max) => {
	if (value < min) return min;
	else if (value > max) return max;
	return value;
};
const loadImage = (url) => {
	return new Promise((resolve) => {
		const img = new Image();
		img.src = url;
		img.crossOrigin = "Anonymous";
		img.addEventListener("load", (e) => {
			resolve(e.target);
		});
	});
};
async function getSquareImage(url, sideLength) {
	const image = await loadImage(url).then((r) => r);

	const canvas = document.createElement("canvas");
	const ctx = canvas.getContext("2d");

	const ratio =
		image.height / image.width >= 1
			? sideLength / image.width
			: sideLength / image.height;
	canvas.width = sideLength; //image.width
	canvas.height = sideLength; //image.width

	ctx.drawImage(
		image,
		sideLength / 2 - (image.width * ratio) / 2,
		sideLength / 2 - (image.height * ratio) / 2,
		image.width * ratio,
		image.height * ratio
	);

	const result = canvas.toDataURL("image/jpeg");
	//console.log(result)

	return result;
}
async function divideImage(url, rows, cols) {
	const canvas = document.createElement("canvas");

	const ctx = canvas.getContext("2d");

	// const squareImageUrl = await getSquareImage(url).then(r => r)

	const image = await loadImage(url).then((r) => r);
	//console.log(image)

	canvas.width = image.width / rows;
	canvas.height = image.height / cols;

	const dividedImages = [];

	let count = 0;

	for (let i = 0; i < cols; i++) {
		for (let j = 0; j < rows; j++) {
			ctx.drawImage(
				image,
				(j * image.width) / rows,
				(i * image.height) / cols,
				image.width / rows,
				image.height / cols,
				0,
				0,
				canvas.width,
				canvas.height
			);

			const clipedImage = canvas.toDataURL("image/jpeg");

			dividedImages[count] = clipedImage;

			count++;
		}
	}
	//console.log(dividedImages)
	return dividedImages;
}

const initialState = {
	isLock: true,
	isShuffle: false,
	isComplete: false,
	bg: null,
	pieces: [],
	pieceLength: 0,
	width: 0,
	height: 0,
	trouble: 0
};

const slidePuzzleReducer = (state, action) => {
	switch (action.type) {
		case "RESIZE_SLIDE_PUZZLE": {
			if (!state.pieces.length) {
				return {
					...state
				};
			}

			const pieces = [...state.pieces];
			const pieceLength = state.pieceLength;
			const width = action.width;
			const height = action.height;

			const rows = Math.sqrt(pieceLength);
			const cols = rows;

			let count = 0;

			for (let i = 0; i < cols; i++) {
				for (let j = 0; j < rows; j++) {
					const pieceWidth = width / rows;
					const pieceHeight = height / cols;

					pieces[count].x *= pieceWidth / pieces[count].width;
					pieces[count].y *= pieceHeight / pieces[count].height;
					pieces[count].basePointX *= pieceWidth / pieces[count].width;
					pieces[count].basePointY *= pieceHeight / pieces[count].height;
					pieces[count].height = pieceHeight;
					pieces[count].width = pieceWidth;

					count++;
				}
			}

			return {
				...state,
				pieces,
				height,
				width
			};
		}

		case "RESET_SLIDE_PUZZLE": {
			return {
				...state,
				pieces: [],
				pieceLength: 0,
				bg: null,
				isLock: true,
				isComplete: false,
				isShuffle: false,
				width: 0,
				height: 0,
				trouble: 0
			};
		}

		case "INITIALIZE_PIECES": {
			const pieces = [...state.pieces];
			const pieceLength = state.pieceLength;

			const height = action.height;
			const width = action.width;

			const rows = Math.sqrt(pieceLength);
			const cols = rows;

			const pieceImages = action.pieceImages;

			let count = 0;

			for (let i = 0; i < cols; i++) {
				for (let j = 0; j < rows; j++) {
					const pieceHeight = height / cols;
					const pieceWidth = width / rows;

					const x = j * pieceHeight;
					const y = i * pieceHeight;

					pieces[count] = {
						...pieces[count],
						id: count,
						baseId: count,
						image: pieceImages[count],
						x,
						y,
						basePointX: x,
						basePointY: y,
						blank: count === pieceLength - 1 ? true : false,
						isDrag: false,
						isShuffle: false,
						height: pieceHeight,
						width: pieceWidth,
						offsetX: 0,
						offsetY: 0
					};

					count++;
				}
			}

			return {
				...state,
				pieces
			};
		}

		case "SHUFFLE_PIECES": {
			const pieces = [...state.pieces];

			pieces.map((piece) => ({
				...piece,
				isShuffle: false
			}));

			const pieceLength = pieces.length;

			const blank = pieces.find((piece) => piece.blank);

			const piecesNextToBlank = pieces.filter((piece, i) => {
				return (
					(piece.id === blank.id - 1 && blank.y === piece.y) || ///Math.sqrt(pieceLength) * piece.width - piece.width) ||
					(piece.id === blank.id + 1 && blank.y === piece.y) || // && piece.x !== 0を追加して空白の次が一行ずれていたらだめ
					(piece.id === blank.id - Math.sqrt(pieceLength) && blank.x === piece.x) ||
					(piece.id === blank.id + Math.sqrt(pieceLength) && blank.x === piece.x)
				);
			});

			console.log(piecesNextToBlank);

			const r = Math.floor(Math.random() * piecesNextToBlank.length);
			console.log(r);
			const movingPiece = piecesNextToBlank[r];
			console.log(movingPiece);

			if (!movingPiece) {
				return {
					...state,
					pieces
				};
			}

			if (movingPiece.isShuffle) {
				return {
					...state,
					pieces: pieces.map((piece) => ({
						...piece,
						isShuffle: false
					}))
				};
			}

			return {
				...state,
				pieces: pieces.map((piece) =>
					piece === movingPiece
						? {
								...piece,
								id: blank.id,
								x: blank.x,
								y: blank.y,
								basePointX: blank.x,
								basePointY: blank.y,
								isShuffle: true
						  }
						: piece === blank
						? {
								...piece,
								id: movingPiece.id,
								x: movingPiece.x,
								y: movingPiece.y,
								basePointX: movingPiece.x,
								basePointY: movingPiece.y,
								isShuffle: false
						  }
						: piece
				)
			};
		}

		case "SORT_PIECES": {
			const pieces = [...state.pieces].sort((a, b) => a.id - b.id);

			return {
				...state,
				pieces
			};
		}

		case "DRAG_START_PIECE": {
			const pieces = [...state.pieces].map((piece) =>
				piece.baseId === action.num
					? {
							...piece,
							isDrag: true
					  }
					: piece
			);

			return {
				...state,
				pieces
			};
		}

		case "LIMIT_MOVEMENT_PIECE": {
			const pieces = [...state.pieces];
			const width = state.width;
			const height = state.height;

			pieces.forEach((piece) => {
				if (piece.isDrag) {
					if (piece.x <= 0) {
						piece.offsetX = 0;
					}

					if (piece.x + piece.width >= width) {
						piece.offsetX = 0;
					}

					if (piece.y <= 0) {
						piece.offsetY = 0;
					}

					if (piece.y + piece.height >= height) {
						piece.offsetY = 0;
					}
				}
			});

			return {
				...state,
				pieces
			};
		}

		case "DRAG_MOVE_PIECE": {
			const pieces = [...state.pieces];
			const pieceLength = pieces.length;
			const elementOffsetX = action.elementOffsetX;
			const elementOffsetY = action.elementOffsetY;

			pieces.forEach((piece) => {
				if (piece.isDrag) {
					if (
						pieces[piece.id - 1] &&
						pieces[piece.id - 1].blank &&
						pieces[piece.id - 1].y === piece.y
					) {
						console.log("is-left");
						pieces[piece.id] = {
							...piece,
							offsetX: clamp(elementOffsetX, -piece.width, 0)
						};
					} else if (
						pieces[piece.id + 1] &&
						pieces[piece.id + 1].blank &&
						pieces[piece.id + 1].y === piece.y //同じ行であれば。例えば2, 3は番号でいえば隣り合っているけどピースは1行ずれているからダメ
					) {
						console.log("is-right");

						pieces[piece.id] = {
							...piece,
							offsetX: clamp(elementOffsetX, 0, piece.width)
						};
					} else if (
						pieces[piece.id - Math.sqrt(pieceLength)] &&
						pieces[piece.id - Math.sqrt(pieceLength)].blank &&
						pieces[piece.id - Math.sqrt(pieceLength)].x === piece.x
					) {
						console.log("is-top");

						pieces[piece.id] = {
							...piece,
							offsetY: clamp(elementOffsetY, -piece.height, 0)
						};
					} else if (
						pieces[piece.id + Math.sqrt(pieceLength)] &&
						pieces[piece.id + Math.sqrt(pieceLength)].blank &&
						pieces[piece.id + Math.sqrt(pieceLength)].x === piece.x
					) {
						console.log("is-bottom");

						pieces[piece.id] = {
							...piece,
							offsetY: clamp(elementOffsetY, 0, piece.height)
						};
					}
				}
			});

			return {
				...state,
				pieces
			};
		}

		case "DRAG_END_PIECE": {
			const pieces = [...state.pieces];
			const pieceLength = pieces.length;

			const replacePiece = (targetPiece, blank) => {
				pieces[targetPiece.id] = {
					...pieces[targetPiece.id],
					id: blank.id,
					x: blank.basePointX,
					y: blank.basePointY,
					basePointX: blank.basePointX,
					basePointY: blank.basePointY,
					offsetX: 0,
					offsetY: 0
				};

				pieces[blank.id] = {
					...pieces[blank.id],
					id: targetPiece.id,
					x: targetPiece.basePointX,
					y: targetPiece.basePointY,
					basePointX: targetPiece.basePointX,
					basePointY: targetPiece.basePointY,
					offsetX: 0,
					offsetY: 0
				};
			};

			let trouble = 0;

			pieces.forEach((piece) => {
				if (piece.isDrag) {
					if (
						Math.abs(piece.offsetX) > piece.width / 2 ||
						Math.abs(piece.offsetY) > piece.height / 2
					) {
						if (piece.offsetX < 0) {
							const blank = pieces[piece.id - 1];
							replacePiece(piece, blank);
						} else if (piece.offsetX > 0) {
							const blank = pieces[piece.id + 1];
							replacePiece(piece, blank);
						} else if (piece.offsetY < 0) {
							const blank = pieces[piece.id - Math.sqrt(pieceLength)];
							replacePiece(piece, blank);
						} else if (piece.offsetY > 0) {
							const blank = pieces[piece.id + Math.sqrt(pieceLength)];
							replacePiece(piece, blank);
						}

						trouble = state.trouble + 1;
					} else {
						piece.offsetX = 0;
						piece.offsetY = 0;

						trouble = state.trouble;
					}
				}
			});

			return {
				...state,
				pieces: pieces.map((piece) =>
					piece.isDrag
						? {
								...piece,
								isDrag: false
						  }
						: piece
				),
				trouble
			};
		}

		case "CHANGE_SLIDE_PUZZLE_BG": {
			return {
				...state,
				bg: action.url
			};
		}

		case "CHANGE_PIECE_LENGTH": {
			return {
				...state,
				pieceLength: action.num
			};
		}

		case "INITIALIZE_SLIDE_PUZZLE": {
			const pieces = [...state.pieces];
			const pieceLength = action.pieceLength;
			const height = action.height;
			const width = action.width;
			const pieceImages = action.pieceImages;

			const rows = Math.sqrt(pieceLength);
			const cols = rows;

			let count = 0;

			for (let i = 0; i < cols; i++) {
				for (let j = 0; j < rows; j++) {
					const pieceHeight = height / cols;
					const pieceWidth = width / rows;

					const x = j * pieceHeight;
					const y = i * pieceHeight;

					pieces[count] = {
						...pieces[count],
						id: count,
						baseId: count,
						image: pieceImages[count],
						x,
						y,
						basePointX: x,
						basePointY: y,
						blank: count === pieceLength - 1 ? true : false,
						isDrag: false,
						isShuffle: false,
						height: pieceHeight,
						width: pieceWidth,
						offsetX: 0,
						offsetY: 0
					};

					count++;
				}
			}

			return {
				...state,
				pieces,
				pieceLength
			};
		}

		case "START_SHUFFLE_PIECES": {
			return {
				...state,
				isShuffle: true
			};
		}

		case "STOP_SHUFFLE_PIECES": {
			const pieces = [...state.pieces].map((piece) => ({
				...piece,
				isShuffle: false
			}));

			return {
				...state,
				pieces,
				isShuffle: false
			};
		}

		case "LOCK_SLIDE_PUZZLE": {
			return {
				...state,
				isLock: true
			};
		}

		case "UNLOCK_SLIDE_PUZZLE": {
			return {
				...state,
				isLock: false
			};
		}

		case "COMPLETE_SLIDE_PUZZLE": {
			return {
				...state,
				isComplete: true
			};
		}

		case "INCOMPLETE_SLIDE_PUZZLE": {
			return {
				...state,
				isComplete: false
			};
		}

		case "RESET_TROUBLE": {
			return {
				...state,
				trouble: 0
			};
		}

		default:
			throw new Error();
	}
};

const useDragAndDrop = () => {
	const [elementPosition, setElementPosition] = useState({ top: 0, left: 0 });
	const [elementOffset, setElementOffset] = useState({ x: 0, y: 0 });

	const pointerStartPosition = useRef({ x: null, y: null });
	const pointerMovePosition = useRef({ x: null, y: null });

	const currentDragElement = useRef(null);

	const prevElementOffset = useRef({ x: 0, y: 0 });

	const prevElementOffsetX = useRef(0);
	const prevElementOffsetY = useRef(0);

	const getCurrentPosition = (elem) => {
		const { top, left } = elem.getBoundingClientRect();
		return { top, left };
	};

	const moveDistance = (distance) =>
		setElementOffset({
			x: prevElementOffset.current.x + distance.x,
			y: prevElementOffset.current.y + distance.y
		});

	const resetElementOffset = () => {
		setElementOffset({
			x: 0,
			y: 0
		});

		prevElementOffset.current = {
			x: 0,
			y: 0
		};
	};

	const resetPointerStartPosition = () => {
		if (
			pointerStartPosition.current.x === null ||
			pointerStartPosition.current.y === null
		)
			return;

		pointerStartPosition.current.x = null;
		pointerStartPosition.current.y = null;
	};

	const handleMouseDown = (e) => {
		e.preventDefault();

		pointerStartPosition.current.x = e.clientX;
		pointerStartPosition.current.y = e.clientY;

		currentDragElement.current = e.target;

		const elementCurrentPosition = getCurrentPosition(currentDragElement.current);

		setElementPosition({
			top: elementCurrentPosition.top,
			left: elementCurrentPosition.left
		});
	};

	const handleMouseMove = (e) => {
		e.preventDefault();

		if (!currentDragElement.current) return;
		console.log("suga move: ", currentDragElement.current);
		if (
			pointerStartPosition.current.x === null ||
			pointerStartPosition.current.y === null
		)
			return;

		pointerMovePosition.current.x = e.clientX;
		pointerMovePosition.current.y = e.clientY;

		const pointerMoveDistance = {
			x: pointerMovePosition.current.x - pointerStartPosition.current.x,
			y: pointerMovePosition.current.y - pointerStartPosition.current.y
		};

		moveDistance(pointerMoveDistance);
	};

	const handleMouseUp = (e) => {
		e.preventDefault();

		if (!currentDragElement.current) return;
		console.log("suga up: ", currentDragElement.current);
		resetPointerStartPosition();

		const elementCurrentPosition = getCurrentPosition(currentDragElement.current);

		setElementPosition({
			top: elementCurrentPosition.top,
			left: elementCurrentPosition.left
		});

		currentDragElement.current = null;
	};

	useEffect(() => {
		document.body.addEventListener("mousemove", handleMouseMove);
		document.body.addEventListener("mouseup", handleMouseUp);
		document.body.addEventListener("mouseleave", handleMouseUp);

		return () => {
			document.body.removeEventListener("mousemove", handleMouseMove);
			document.body.removeEventListener("mouseup", handleMouseUp);
			document.body.removeEventListener("mouseleave", handleMouseUp);
		};
	}, []);

	useEffect(() => {
		prevElementOffset.current = {
			x: elementOffset.x,
			y: elementOffset.y
		};
	}, [elementPosition.left, elementPosition.top]);

	return [
		{
			currentDragElement,
			elementPosition,
			elementOffset
		},
		{
			pointerStartPosition,
			pointerMovePosition
		},
		handleMouseDown,
		setElementOffset,
		resetElementOffset,
		setElementPosition
	];
};
const useResizeObserver = (elements, callback) => {
	useEffect(() => {
		console.log("mari");
		const resizeObserver = new ResizeObserver((entries) => callback(entries));

		for (const elem of elements) {
			//console.log(elem);
			elem.current && resizeObserver.observe(elem.current);
		}

		return () => resizeObserver.disconnect();
	}, []);
};
const useMutationObserver = (elements, callback, config) => {
	useEffect(() => {
		const mutationObserver = new MutationObserver((mutations) => {
			mutationObserver.disconnect();
			callback(mutations);
			for (const elem of elements) {
				elem.current && mutationObserver.observe(elem.current, config);
			}
		});

		for (const elem of elements) {
			elem.current && mutationObserver.observe(elem.current, config);
		}

		return () => mutationObserver.disconnect();
	}, []);
};
const useSlidePuzzle = (bg, pieceLength) => {
	const [
		element,
		pointer,
		handleMouseDown,
		setElementOffset,
		resetElementOffset,
		setElementPosition
	] = useDragAndDrop();

	const { currentDragElement, elementPosition, elementOffset } = element;

	const { pointerStartPosition, pointerMovePosition } = pointer;

	const [slidePuzzle, dispatch] = useReducer(slidePuzzleReducer, initialState);

	const slidePuzzleNode = useRef(null);
	const intervalId = useRef(null);

	const initializeSlidePuzzle = async () => {
		// https://shanabrian.com/web/javascript/element-client-width-height.php
		const width = slidePuzzleNode.current.clientWidth;
		const height = slidePuzzleNode.current.clientHeight;

		const rows = Math.sqrt(pieceLength);
		const cols = rows;
		const squareImageUrl = await getSquareImage(bg, width).then((r) => r);
		const pieceImages = divideImage(squareImageUrl, rows, cols);

		pieceImages.then((images) =>
			dispatch({
				type: "INITIALIZE_SLIDE_PUZZLE",
				pieceLength,
				pieceImages: images,
				height,
				width
			})
		);
	};
	/*const initializePieces = useCallback(() => {
		const rect = slidePuzzleNode.current;

		const pieceLength = slidePuzzle.pieceLength;
		const bg = slidePuzzle.bg;

		// https://shanabrian.com/web/javascript/element-client-width-height.php
		const width = rect.clientWidth; //width//puzzle.width
		const height = rect.clientHeight; //puzzle.height

		const rows = Math.sqrt(pieceLength);
		const cols = rows;

		const pieceImages = divideImage(bg, rows, cols);

		pieceImages.then((images) =>
			dispatch({
				type: "INITIALIZE_PIECES",
				//pieceLength,
				pieceImages: images,
				height,
				width
			})
		);
	}, [
		slidePuzzle.bg,
		slidePuzzle.pieceLength,
		slidePuzzle.width,
		slidePuzzle.height,
		slidePuzzle.pieceImages
	]);*/

	const changeSlidePuzzleBg = useCallback((url) =>
		dispatch({
			type: "CHANGE_SLIDE_PUZZLE_BG",
			url
		})
	);

	const changePieceLength = useCallback((num) =>
		dispatch({
			type: "CHANGE_PIECE_LENGTH",
			num
		})
	);

	const startShufflePieces = useCallback(() =>
		dispatch({
			type: "START_SHUFFLE_PIECES"
		})
	);

	const stopShufflePieces = useCallback(() =>
		//clearInterval(intervalId.current)
		dispatch({
			type: "STOP_SHUFFLE_PIECES"
		})
	);

	const lockSlidePuzzle = useCallback(() =>
		dispatch({
			type: "LOCK_SLIDE_PUZZLE"
		})
	);
	const unlockSlidePuzzle = useCallback(() =>
		dispatch({
			type: "UNLOCK_SLIDE_PUZZLE"
		})
	);

	const completeSlidePuzzle = useCallback(() =>
		dispatch({
			type: "COMPLETE_SLIDE_PUZZLE"
		})
	);
	const incompleteSlidePuzzle = useCallback(() =>
		dispatch({
			type: "INCOMPLETE_SLIDE_PUZZLE"
		})
	);

	const resetSlidePuzzle = useCallback(() =>
		dispatch({
			type: "RESET_SLIDE_PUZZLE"
		})
	);

	const resetTrouble = useCallback(() =>
		dispatch({
			type: "RESET_TROUBLE"
		})
	);

	useResizeObserver([slidePuzzleNode], (entries) => {
		const height = entries[0].contentRect.height;
		const width = entries[0].contentRect.width;

		if (height === 0 || width === 0) return;

		dispatch({
			type: "RESIZE_SLIDE_PUZZLE",
			width,
			height
		});
	});

	useEffect(() => {
		if (!slidePuzzle.isShuffle) return;

		let count = 0;

		intervalId.current = setInterval(() => {
			count++;

			dispatch({
				type: "SHUFFLE_PIECES"
			});

			if (count > MAX_SHUFFLE_COUNT) {
				clearInterval(intervalId.current);

				dispatch({
					type: "SORT_PIECES"
				});

				dispatch({
					type: "STOP_SHUFFLE_PIECES"
				});

				dispatch({
					type: "UNLOCK_SLIDE_PUZZLE"
				});
			}
		}, SHUFFLE_SPEED);
	}, [slidePuzzle.isShuffle]);

	/*useEffect(() => {
		if (slidePuzzle.isShuffle) return;
		clearInterval(intervalId.current);
	}, [slidePuzzle.isShuffle]);*/

	// mousedown
	useEffect(() => {
		if (slidePuzzle.isLock) return;

		if (!currentDragElement.current) return;

		const num = parseFloat(
			currentDragElement.current.className.replace(/[^0-9]/g, "")
		);

		dispatch({
			type: "DRAG_START_PIECE",
			num
		});
	}, [elementPosition]);

	// mousemove
	useEffect(() => {
		if (slidePuzzle.isLock) return;

		if (!currentDragElement.current) return;

		dispatch({
			type: "LIMIT_MOVEMENT_PIECE"
		});

		dispatch({
			type: "DRAG_MOVE_PIECE",
			elementOffsetX: elementOffset.x,
			elementOffsetY: elementOffset.y
		});
	}, [elementOffset.x, elementOffset.y]);
	//const [pieces, setPieces] = useState([])
	/*useMutationObserver(
    [slidePuzzleNode],
    (mutations) => {
      setPieces(pieces => {
        pieces.forEach(piece => {
        if(piece.isDrag){
          if(
          pieces[piece.id - 1] &&
          pieces[piece.id - 1].blank && 
          pieces[piece.id - 1].y === piece.y
          ) {
          console.log('is-left')
          pieces[piece.id] = {
            ...piece,
            offsetX: clamp(
            elementOffsetX,
            -piece.width,//pieces[piece.id - 1].x,
            0//pieces[piece.id - 1].x + piece.width
            )
          }
          }
          else if(
          pieces[piece.id + 1] &&
          pieces[piece.id + 1].blank &&
          pieces[piece.id + 1].y === piece.y//同じ行であれば。例えば2, 3は番号でいえば隣り合っているけどピースは1行ずれているからダメ
          ) {
          console.log('is-right')

          pieces[piece.id] = {
            ...piece,
            offsetX: clamp(
            elementOffsetX,
            0,//pieces[piece.id + 1].x - piece.width,
            piece.width//pieces[piece.id + 1].x
            )
          }
          }
          else if(
          pieces[piece.id - Math.sqrt(pieceLength)] &&
          pieces[piece.id - Math.sqrt(pieceLength)].blank &&
          pieces[piece.id - Math.sqrt(pieceLength)].x === piece.x
          ) {
          console.log('is-top')

           pieces[piece.id] = {
            ...piece,
            offsetY: clamp(
            elementOffsetY,
            -piece.height,//pieces[piece.id - Math.sqrt(pieceLength)].y,
            0//pieces[piece.id - Math.sqrt(pieceLength)].y + piece.height
            )
          }
          }
          else if(
          pieces[piece.id + Math.sqrt(pieceLength)] &&
          pieces[piece.id + Math.sqrt(pieceLength)].blank &&
          pieces[piece.id + Math.sqrt(pieceLength)].x === piece.x
          ) {
          console.log('is-bottom')

          pieces[piece.id] = {
            ...piece,
            offsetY: clamp(
            elementOffsetY,
            0,//pieces[piece.id + Math.sqrt(pieceLength)].y - piece.height,
            piece.height//pieces[piece.id + Math.sqrt(pieceLength)].y
            )
          }
          }
        }
        })
      })    
    },
    {
            attributes: true,
            subtree: false,
            childList: false,
            attributeFilter: ["class"]
        }
  )*/

	// mouseup
	useEffect(() => {
		if (slidePuzzle.isLock) return;

		if (currentDragElement.current) return;

		dispatch({
			type: "DRAG_END_PIECE"
		});

		dispatch({
			type: "SORT_PIECES"
		});

		resetElementOffset();
	}, [elementPosition]);

	//素早いmousemoveからのmouseup後にelementOffsetXが更新されてしまうため、ここでもリセット
	/*useEffect(() => {
		if (slidePuzzle.isLock) return;
		if (currentDragElement.current) return;
		console.log("oua: ", elementOffset.x);
		resetElementOffset();
	}, [elementOffset.x, elementOffset.y]);*/

	// mouseup後にpuzzleが変更されたら
	useEffect(() => {
		if (slidePuzzle.isLock) return;
		console.log(currentDragElement.current);
		if (currentDragElement.current) return;

		const matchPieces = slidePuzzle.pieces.filter(
			(piece) => piece.id === piece.baseId
		);
		//console.log(matchPieces)
		console.log(slidePuzzle);
		console.log(matchPieces.length);
		console.log(slidePuzzle.pieceLength);
		if (matchPieces.length === slidePuzzle.pieceLength) {
			console.log("clear");
			dispatch({
				type: "COMPLETE_SLIDE_PUZZLE"
			});
		} else {
			console.log("mada");
		}
	}, [slidePuzzle.pieces]);

	return [
		{
			pieces: slidePuzzle.pieces,
			slidePuzzleNode,
			handleMouseDown
		},
		initializeSlidePuzzle,
		//initializePieces,
		changeSlidePuzzleBg,
		changePieceLength,
		startShufflePieces,
		stopShufflePieces,
		lockSlidePuzzle,
		unlockSlidePuzzle,
		slidePuzzle.isComplete,
		completeSlidePuzzle,
		incompleteSlidePuzzle,
		resetSlidePuzzle,
		slidePuzzle.trouble,
		resetTrouble
	];
};

const GlobalStyle = createGlobalStyle`
		@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');

:root {
    --base-color: burlywood;
    --accent-color: darkorange;
    --text-color: rgb(40, 40, 40);
  }

	* {
	  font-family: 'Press Start 2P', cursive;
	}
	
	*,
	*::before,
	*::after {
		box-sizing: border-box;
	}

	body {
		background-color: var(--base-color);
		color: var(--text-color);
	 	margin: 0;
	}
`;

const Spacer = ({ size, horizontal }) => {
	return (
		<div
			style={
				horizontal
					? { width: size, height: "auto", display: "inline-block", flexShrink: 0 }
					: { width: "auto", height: size, flexShrink: 0 }
			}
		/>
	);
};

const StyledSlidePuzzle = styled.div`
	background-color: ${({ boardColor }) => boardColor};
	border: 1vmin solid ${({ boardColor }) => boardColor};
	border-radius: 1vmin;
	height: ${({ sideLength }) => sideLength};
	position: relative;
	width: ${({ sideLength }) => sideLength};
	z-index: 0;
`;

const Piece = styled.div`
	//background-color: ${({ boardColor }) => boardColor};//#997f5d;//#333;
	box-sizing: border-box;
	//border: .3vmin solid ${({ boardColor }) => boardColor};//#997f5d;//#333;
	height: ${({ piece }) => piece.height}px;
	padding: 0.3vmin;
	position: absolute;
	left: ${({ piece }) => piece.x}px;
	top: ${({ piece }) => piece.y}px;
	transition: ${({ piece }) => (piece.isShuffle ? `all linear 100ms` : "")};
	transform: translate3d(
		${({ piece }) => piece.offsetX}px,
		${({ piece }) => piece.offsetY}px,
		0
	);
	user-select: none;
	width: ${({ piece }) => piece.width}px;
	z-index: ${({ piece }) => (piece.blank ? 1 : 3)};
`;

const Img = styled.img`
  display: block
  height: auto;
  pointer-events: none;
  width: 100%;
`;

const Num = styled.span`
	color: white;
	font-size: 1vmin;
	position: absolute;
	top: 1.2vmin; //.6vmin;
	left: 1.2vmin;
`;

const SlidePuzzleWrapper = styled.div`
	//height: 60vmin;
	//width: 60vmin;
`;

const SlidePuzzle = ({ className, slidePuzzle, boardColor, sideLength }) => (
	<StyledSlidePuzzle
		className={className}
		ref={slidePuzzle.slidePuzzleNode}
		boardColor={boardColor}
		sideLength={sideLength}
	>
		{slidePuzzle.pieces.map((piece, i) => (
			<Piece
				className={`piece piece-${piece.baseId}`}
				piece={piece}
				onMouseDown={slidePuzzle.handleMouseDown}
				key={piece.baseId}
			>
				{!piece.blank && (
					<React.Fragment>
						<Num className="piece-num">{piece.baseId}</Num>
						<Img className="piece-image" src={piece.image}></Img>
					</React.Fragment>
				)}
			</Piece>
		))}
	</StyledSlidePuzzle>
);

const JustifyContentSpaceBetween = styled.div`
	display: flex;
	justify-content: space-between;
`;

const JustifyContentCenter = styled.div`
	display: flex;
	justify-content: center;
`;

const StyledGameStatus = styled.div`
	color: ${({ color }) => (color ? color : "#333333")};
	font-size: ${({ size }) => (size ? size : "1em")};
`;

const GameStatus = ({ className, title, text, color, size }) => (
	<StyledGameStatus
		className={className}
		color={color}
		size={size}
	>{`${title}: ${text}`}</StyledGameStatus>
);

const Button = styled.button`
	background-color: ${({ colors }) => (colors && colors.bg) || "lightgray"};
	border: none;
	border-radius: 0.6vmin;
	cursor: ${({ isDisabled }) => (isDisabled ? "cursor" : "pointer")};
	color: ${({ colors }) => (colors && colors.text) || "#333333"};
	font-size: ${({ sizes }) => (sizes && sizes.font) || "1em"};
	height: ${({ sizes }) => (sizes && sizes.height) || "auto"};
	outline: none;
	opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)};
	padding: ${({ sizes }) => (sizes && sizes.padding) || "1em 2em"};
	pointer-events: ${({ isDisabled }) => (isDisabled ? "none" : "auto")};
	user-select: none;
	width: ${({ sizes }) => (sizes && sizes.width) || "auto"};
`;

const StyledModalWindow = styled.div`
	z-index: 1000;

	& .content-wrapper {
	}
`;

const Overlay = styled.div`
	background-color: rgba(0, 0, 0, 0.6);
	position: fixed;
	top: -100vh;
	left: -100vw;
	bottom: -100vh;
	right: -100vw;
	z-index: 1;
`;

const ContentWrapper = styled.div`
	background-color: white;
	box-sizing: border-box;
	border-radius: 2vmin;
	color: #333;
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: 60vmin;
	text-align: center;
	position: fixed;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	min-width: 80vmin;
	z-index: 2;
	> button {
		position: absolute;
		top: 4vmin;
		right: 4vmin;
	}
`;

const Content = styled.div`
	max-height: 80vmin;
	padding: 4vmin;
`;

const Title = styled.h1`
	font-size: 4vmin;
	margin: 0 auto 4vmin;
`;

const Text = styled.p`
	color: #777;
	font-size: 2vmin;
	margin: 0 0 4vmin;
`;

const Buttons = styled.div`
	display: flex;
	justify-content: center;

	> button {
		font-size: 2vmin;
		margin: 0 0.6vmin;
	}
`;

const ModalWindow = ({
	className,
	title,
	text,
	isShow,
	isShowCloseButton,
	buttons
}) => {
	const [isClose, setIsClose] = useState(false);
	const closeButtonSizes = {
		padding: "0"
	};
	return (
		<>
			{isShow ? (
				<StyledModalWindow className={className}>
					<Overlay />
					<ContentWrapper>
						{isShowCloseButton && (
							<Button
								colors={{
									bg: "transparent"
								}}
								sizes={closeButtonSizes}
								onClick={() => setIsClose(true)}
							>
								✖
							</Button>
						)}
						<Content>
							<Title>{title}</Title>
							<Text>{text}</Text>
							<Buttons>
								{buttons.map((b, i) => (
									<Button onClick={b.callback} colors={b.colors} sizes={b.sizes}>
										{b.label}
									</Button>
								))}
							</Buttons>
						</Content>
					</ContentWrapper>
				</StyledModalWindow>
			) : (
				<></>
			)}
		</>
	);
};

const StyledApp = styled.div`
	background-color: burlywood;
	display: flex;
	align-items: center;
	justify-content: center;
	height: 100vh;
	width: 100%;
`;

const App = () => {
	const [isStart, setIsStart] = useState(false);
	const [level, setLevel] = useState(LEVEL.easy);
	const [selectBg, setSelectBg] = useState(BG);
	const [
		slidePuzzle,
		initializeSlidePuzzle,
		//initializePieces,
		changeSlidePuzzleBg,
		changePieceLength,
		startShufflePieces,
		stopShufflePieces,
		lockSlidePuzzle,
		unlockSlidePuzzle,
		isComplete,
		completeSlidePuzzle,
		incompleteSlidePuzzle,
		resetSlidePuzzle,
		trouble,
		resetTrouble
	] = useSlidePuzzle(selectBg, level);

	useEffect(() => {
		initializeSlidePuzzle();
	}, []);

	const initializeGame = useCallback(() => {
		setIsStart(false);
		
	}, [])
	
	const startGame = useCallback(() => {
		setIsStart(true);
		//lockSlidePuzzle();
		startShufflePieces();
	}, []);

	useEffect(() => {
		if (!isComplete) return;
		lockSlidePuzzle(); //resetSlidePuzzle()
		// alert('Game clear!')
	}, [isComplete]);

	const resetGame = useCallback(() => {
		setIsStart(false);
		resetSlidePuzzle();
		//stopShufflePieces()
		initializeSlidePuzzle();
	}, []);

	const buttonSizes = {
		font: "1vmin",
		padding: "2vmin 4vmin"
	};

	const startBtn = {
		label: "start",
		colors: {
			bg: `var(--accent-color)`,
			text: "white"
		},
		callback: startGame
	};

	const endBtn = {
		label: "end",
		colors: {
			bg: "#333",
			text: "white"
		}
		//callback: initializeGame
	};

	const replayBtn = {
		label: "replay",
		colors: {
			bg: "green",
			text: "white"
		}
		//callback: () => {
		//  initializeGame();
		//  startGame();
		// }
	};

	return (
		<>
			<GlobalStyle />
			<StyledApp>
				<ModalWindow
					className="Game clear!"
					title="Time's up"
					text={`your trouble is ${trouble}.`}
					isShow={isComplete}
					isShowCloseButton={false}
					buttons={[endBtn, replayBtn]}
				></ModalWindow>
				<SlidePuzzleWrapper>
					<JustifyContentSpaceBetween>
						<GameStatus
							className="troble"
							title="Trouble"
							text={trouble}
							size="2vmin"
						/>
						<GameStatus
							className="level"
							title="Level"
							text={
								level === LEVEL.easy
									? "easy"
									: level === LEVEL.normal
									? "normal"
									: level === LEVEL.hard
									? "hard"
									: "easy"
							}
							size="2vmin"
						/>
					</JustifyContentSpaceBetween>
					<Spacer size="1.4vmin" />
					<SlidePuzzle
						className="slide-puzzle"
						slidePuzzle={slidePuzzle}
						boardColor={BOARD_COLOR}
						sideLength="60vmin"
					/>
					<Spacer size="3vmin" />
					<JustifyContentCenter>
						<Button
							colors={{
								bg: "var(--accent-color)",
								text: "#fff"
							}}
							sizes={buttonSizes}
							onClick={startGame}
							isDisabled={isStart ? true : false}
						>
							Start
						</Button>
						<Spacer size="1vmin" horizontal={true} />
						<Button
							sizes={buttonSizes}
							onClick={resetGame}
							isDisabled={isStart ? false : true}
						>
							Reset
						</Button>
					</JustifyContentCenter>
				</SlidePuzzleWrapper>
			</StyledApp>
		</>
	);
};

ReactDOM.render(<App />, document.getElementById("root"));
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/styled-components/4.3.1/styled-components.min.js