() => {
        it('should snap left/right edges', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x += 1) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.left).toEqual(visibleBounds.left);
              expect(instance.imageBounds.right).toEqual(visibleBounds.right);
            }
          });
        });

        it('should snap top/bottom edges', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.top).toEqual(visibleBounds.top);
              expect(instance.imageBounds.bottom).toEqual(visibleBounds.bottom);
            }
          });
        });
      }


() => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x += 1) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.left).toEqual(visibleBounds.left);
              expect(instance.imageBounds.right).toEqual(visibleBounds.right);
            }
          });
        }


imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x += 1) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.left).toEqual(visibleBounds.left);
              expect(instance.imageBounds.right).toEqual(visibleBounds.right);
            }
          }


() => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.top).toEqual(visibleBounds.top);
              expect(instance.imageBounds.bottom).toEqual(visibleBounds.bottom);
            }
          });
        }


imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.top).toEqual(visibleBounds.top);
              expect(instance.imageBounds.bottom).toEqual(visibleBounds.bottom);
            }
          }


() => {
        it('should snap left edge', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x++) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.left).toEqual(visibleBounds.left);
            }
          });
        });

        it('should snap top edge', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.top).toEqual(visibleBounds.top);
            }
          });
        });

        it('should snap bottom edge', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.bottom).toEqual(visibleBounds.bottom);
            }
          });
        });

        it('should snap right edge', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x++) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.right).toEqual(visibleBounds.right);
            }
          });
        });
      }


() => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x++) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.left).toEqual(visibleBounds.left);
            }
          });
        }


imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x++) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.left).toEqual(visibleBounds.left);
            }
          }


() => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.top).toEqual(visibleBounds.top);
            }
          });
        }


imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.top).toEqual(visibleBounds.top);
            }
          }


() => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.bottom).toEqual(visibleBounds.bottom);
            }
          });
        }


imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.bottom).toEqual(visibleBounds.bottom);
            }
          }


() => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x++) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.right).toEqual(visibleBounds.right);
            }
          });
        }


imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x++) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.right).toEqual(visibleBounds.right);
            }
          }


() => {
      it('should scale image up/down if imageSourceRect different than visibleBounds when using constraints', () => {
        imageSizes.forEach(imageSize => {
          const { instance } = setup(defaultProps, imageSize, imageSize);
          const { imageBounds } = instance;
          expect(imageBounds.width).toEqual(mediumSize);
          expect(imageBounds.height).toEqual(mediumSize);
        });
      });

      it('should scale image up/down if imageSourceRect different than visibleBounds when not using constraints', () => {
        imageSizes.forEach(imageSize => {
          const { instance } = setup(
            {
              ...defaultProps,
              useConstraints: false,
            },
            imageSize,
            imageSize,
          );
          const { imageBounds } = instance;
          expect(imageBounds.width).toEqual(imageSize);
          expect(imageBounds.height).toEqual(imageSize);
        });
      });

      it('should not apply zoomToFit if zero imageBounds when image loads', () => {
        const { instance } = setup();
        jest.spyOn(instance, 'zoomToFit');
        instance.onImageLoad({} as any, 0, 0);
        expect(instance.zoomToFit).not.toHaveBeenCalled();
      });

      it('should not apply zoomToFit if not using constraints when image loads', () => {
        const { instance } = setup({
          ...defaultProps,
          useConstraints: false,
        });
        jest.spyOn(instance, 'zoomToFit');
        instance.onImageLoad({} as any, smallSize, smallSize);
        expect(instance.zoomToFit).not.toHaveBeenCalled();
      });

      it('should call onZoomChange prop when zoomToFit', () => {
        const { instance, onZoomChange } = setup();
        instance.zoomToFit();
        expect(onZoomChange).toHaveBeenCalledTimes(1);
      });
    }


() => {
        imageSizes.forEach(imageSize => {
          const { instance } = setup(defaultProps, imageSize, imageSize);
          const { imageBounds } = instance;
          expect(imageBounds.width).toEqual(mediumSize);
          expect(imageBounds.height).toEqual(mediumSize);
        });
      }


imageSize => {
          const { instance } = setup(defaultProps, imageSize, imageSize);
          const { imageBounds } = instance;
          expect(imageBounds.width).toEqual(mediumSize);
          expect(imageBounds.height).toEqual(mediumSize);
        }


() => {
        imageSizes.forEach(imageSize => {
          const { instance } = setup(
            {
              ...defaultProps,
              useConstraints: false,
            },
            imageSize,
            imageSize,
          );
          const { imageBounds } = instance;
          expect(imageBounds.width).toEqual(imageSize);
          expect(imageBounds.height).toEqual(imageSize);
        });
      }


imageSize => {
          const { instance } = setup(
            {
              ...defaultProps,
              useConstraints: false,
            },
            imageSize,
            imageSize,
          );
          const { imageBounds } = instance;
          expect(imageBounds.width).toEqual(imageSize);
          expect(imageBounds.height).toEqual(imageSize);
        }


() => {
        const { instance } = setup();
        jest.spyOn(instance, 'zoomToFit');
        instance.onImageLoad({} as any, 0, 0);
        expect(instance.zoomToFit).not.toHaveBeenCalled();
      }


() => {
        const { instance } = setup({
          ...defaultProps,
          useConstraints: false,
        });
        jest.spyOn(instance, 'zoomToFit');
        instance.onImageLoad({} as any, smallSize, smallSize);
        expect(instance.zoomToFit).not.toHaveBeenCalled();
      }


() => {
        const { instance, onZoomChange } = setup();
        instance.zoomToFit();
        expect(onZoomChange).toHaveBeenCalledTimes(1);
      }


() => {
      it('should map equally when imageBounds and visibleBounds equal', () => {
        const { instance } = setup(defaultProps, ...imageSizeMedium);
        const corner = instance.transformVisibleBoundsToImageCoords(
          containerSize,
          containerSize,
        );
        expect(corner.x).toEqual(mediumSize);
        expect(corner.y).toEqual(mediumSize);
      });

      it('should map reduced coords when imageBounds smaller than visibleBounds', () => {
        const { instance } = setup(defaultProps, ...imageSizeSmall);
        const corner = instance.transformVisibleBoundsToImageCoords(
          containerSize,
          containerSize,
        );
        expect(corner.x).toEqual(smallSize);
        expect(corner.y).toEqual(smallSize);
      });

      it('should map enlarged coords when imageBounds larger than visibleBounds', () => {
        const { instance } = setup(defaultProps, ...imageSizeLarge);
        const corner = instance.transformVisibleBoundsToImageCoords(
          containerSize,
          containerSize,
        );
        expect(corner.x).toEqual(largeSize);
        expect(corner.y).toEqual(largeSize);
      });
    }


() => {
        const { instance } = setup(defaultProps, ...imageSizeMedium);
        const corner = instance.transformVisibleBoundsToImageCoords(
          containerSize,
          containerSize,
        );
        expect(corner.x).toEqual(mediumSize);
        expect(corner.y).toEqual(mediumSize);
      }


() => {
        const { instance } = setup(defaultProps, ...imageSizeSmall);
        const corner = instance.transformVisibleBoundsToImageCoords(
          containerSize,
          containerSize,
        );
        expect(corner.x).toEqual(smallSize);
        expect(corner.y).toEqual(smallSize);
      }


() => {
        const { instance } = setup(defaultProps, ...imageSizeLarge);
        const corner = instance.transformVisibleBoundsToImageCoords(
          containerSize,
          containerSize,
        );
        expect(corner.x).toEqual(largeSize);
        expect(corner.y).toEqual(largeSize);
      }


() => {
      it('should map coords correctly when zoomed out', () => {
        const { instance } = setup(defaultProps, ...imageSizeMedium);
        const sourceBounds = instance.sourceBounds;
        expect(sourceBounds.left).toEqual(0);
        expect(sourceBounds.top).toEqual(0);
        expect(sourceBounds.width).toEqual(mediumSize);
        expect(sourceBounds.height).toEqual(mediumSize);
      });

      it('should map coords correctly when zoomed in', async done => {
        const { instance } = setup(defaultProps, ...imageSizeMedium);
        instance.setZoom(1);
        await nextTick();
        const sourceBounds = instance.sourceBounds;
        expect(instance.state.zoom).toEqual(1);
        expect(sourceBounds.left).toEqual(Math.round(mediumSize / 4));
        expect(sourceBounds.top).toEqual(Math.round(mediumSize / 4));
        expect(sourceBounds.width).toEqual(mediumSize / defaultMaxZoom);
        expect(sourceBounds.height).toEqual(mediumSize / defaultMaxZoom);
        done();
      });
    }


() => {
        const { instance } = setup(defaultProps, ...imageSizeMedium);
        const sourceBounds = instance.sourceBounds;
        expect(sourceBounds.left).toEqual(0);
        expect(sourceBounds.top).toEqual(0);
        expect(sourceBounds.width).toEqual(mediumSize);
        expect(sourceBounds.height).toEqual(mediumSize);
      }


async done => {
        const { instance } = setup(defaultProps, ...imageSizeMedium);
        instance.setZoom(1);
        await nextTick();
        const sourceBounds = instance.sourceBounds;
        expect(instance.state.zoom).toEqual(1);
        expect(sourceBounds.left).toEqual(Math.round(mediumSize / 4));
        expect(sourceBounds.top).toEqual(Math.round(mediumSize / 4));
        expect(sourceBounds.width).toEqual(mediumSize / defaultMaxZoom);
        expect(sourceBounds.height).toEqual(mediumSize / defaultMaxZoom);
        done();
      }


() => {
    it('should set zoom state when zoom prop changes', () => {
      const onZoomChange = jest.fn();
      const { instance, wrapper } = setup({
        ...defaultProps,
        onZoomChange,
      });
      jest.spyOn(instance, 'setZoom');
      wrapper.setProps({
        zoom: 0.5,
      });
      expect(instance.setZoom).toHaveBeenCalledWith(0.5);
      expect(wrapper.state('zoom')).toEqual(0.5);
    });

    it('should reset zoom and imageBounds when useConstraints prop changes', () => {
      const { instance, wrapper } = setup(defaultProps, mediumSize, mediumSize);
      wrapper.setState({
        imageWidth: largeSize,
        imageHeight: largeSize,
        zoom: 0.5,
      });
      wrapper.setProps({
        useConstraints: false,
      });
      const { zoom, imageWidth, imageHeight } = instance.state;
      expect(zoom).toEqual(0);
      expect(imageWidth).toEqual(mediumSize);
      expect(imageHeight).toEqual(mediumSize);
    });

    it('should reset zoom when containerWidth prop changes', () => {
      const { wrapper, onZoomChange } = setup({
        ...defaultProps,
        zoom: 0.5,
      });
      wrapper.setProps({
        containerWidth: largeSize,
      });
      expect(onZoomChange).toHaveBeenCalled();
      expect(wrapper.state('zoom')).toEqual(0);
    });

    it('should reset zoom when containerHeight prop changes', () => {
      const { wrapper, onZoomChange } = setup({
        ...defaultProps,
        zoom: 0.5,
      });
      wrapper.setProps({
        containerHeight: largeSize,
      });
      expect(onZoomChange).toHaveBeenCalled();
      expect(wrapper.state('zoom')).toEqual(0);
    });

    it('should reset zoom and update zoom prop callback when margin prop changes', () => {
      const { wrapper, onZoomChange } = setup({
        ...defaultProps,
        zoom: 0.5,
      });
      wrapper.setProps({
        margin: largeSize,
      });
      expect(onZoomChange).toHaveBeenCalled();
      expect(wrapper.state('zoom')).toEqual(0);
    });

    it('should preprocess image when src prop changes', async done => {
      const { instance, wrapper } = setup();
      instance.preprocessFile = jest.fn().mockResolvedValue(null);
      wrapper.setProps({
        src: 'some-new-src',
      });
      await nextTick();
      expect(instance.preprocessFile).toHaveBeenCalled();
      done();
    });

    it('should preprocess image when file prop changes', async done => {
      const { instance, wrapper } = setup();
      instance.preprocessFile = jest.fn().mockResolvedValue(null);
      wrapper.setProps({
        src: 'some-new-src',
      });
      await nextTick();
      expect(instance.preprocessFile).toHaveBeenCalled();
      done();
    });

    it('should clear error state when new src or file given', async done => {
      const { wrapper, instance } = setup();
      instance.onImageError('some-error');
      expect(wrapper.state('errorMessage')).not.toBeUndefined();
      instance.setSrc({ file: {} as File, src: 'some-src' });
      expect(wrapper.state('errorMessage')).toBeUndefined();

      done();
    });
  }


() => {
      const onZoomChange = jest.fn();
      const { instance, wrapper } = setup({
        ...defaultProps,
        onZoomChange,
      });
      jest.spyOn(instance, 'setZoom');
      wrapper.setProps({
        zoom: 0.5,
      });
      expect(instance.setZoom).toHaveBeenCalledWith(0.5);
      expect(wrapper.state('zoom')).toEqual(0.5);
    }


() => {
      const { instance, wrapper } = setup(defaultProps, mediumSize, mediumSize);
      wrapper.setState({
        imageWidth: largeSize,
        imageHeight: largeSize,
        zoom: 0.5,
      });
      wrapper.setProps({
        useConstraints: false,
      });
      const { zoom, imageWidth, imageHeight } = instance.state;
      expect(zoom).toEqual(0);
      expect(imageWidth).toEqual(mediumSize);
      expect(imageHeight).toEqual(mediumSize);
    }


() => {
      const { wrapper, onZoomChange } = setup({
        ...defaultProps,
        zoom: 0.5,
      });
      wrapper.setProps({
        containerWidth: largeSize,
      });
      expect(onZoomChange).toHaveBeenCalled();
      expect(wrapper.state('zoom')).toEqual(0);
    }


() => {
      const { wrapper, onZoomChange } = setup({
        ...defaultProps,
        zoom: 0.5,
      });
      wrapper.setProps({
        containerHeight: largeSize,
      });
      expect(onZoomChange).toHaveBeenCalled();
      expect(wrapper.state('zoom')).toEqual(0);
    }


() => {
      const { wrapper, onZoomChange } = setup({
        ...defaultProps,
        zoom: 0.5,
      });
      wrapper.setProps({
        margin: largeSize,
      });
      expect(onZoomChange).toHaveBeenCalled();
      expect(wrapper.state('zoom')).toEqual(0);
    }


async done => {
      const { instance, wrapper } = setup();
      instance.preprocessFile = jest.fn().mockResolvedValue(null);
      wrapper.setProps({
        src: 'some-new-src',
      });
      await nextTick();
      expect(instance.preprocessFile).toHaveBeenCalled();
      done();
    }


async done => {
      const { wrapper, instance } = setup();
      instance.onImageError('some-error');
      expect(wrapper.state('errorMessage')).not.toBeUndefined();
      instance.setSrc({ file: {} as File, src: 'some-src' });
      expect(wrapper.state('errorMessage')).toBeUndefined();

      done();
    }


() => {
    const mockFileInfo = { file: {}, src: '' } as FileInfo;
    const containerRect = new Rectangle(
      defaultProps.containerWidth,
      defaultProps.containerHeight,
    );

    afterEach(() => {
      unMockLoadImage();
    });

    it('should return null if error occurs when loading image during image initialisation', async () => {
      mockLoadImageError();

      const imageInfo = await initialiseImagePreview(
        mockFileInfo,
        containerRect,
        defaultProps.maxZoom,
      );
      expect(imageInfo).toBeNull();
    });

    it('should scale down image to fit largest zoom size required', async done => {
      mockLoadImage(extraLargeSize, extraLargeSize);

      const imageInfo = await initialiseImagePreview(
        mockFileInfo,
        containerRect,
        defaultProps.maxZoom,
      );
      if (imageInfo !== null) {
        expect(imageInfo.width).toEqual(
          defaultProps.containerWidth * defaultProps.maxZoom,
        );
        expect(imageInfo.height).toEqual(
          defaultProps.containerHeight * defaultProps.maxZoom,
        );
        done();
      }
    });

    describe('Rotate imageSourceRect when Exif orientation', () => {
      const shortSide = mediumSize;
      const longSide = largeSize;

      afterEach(() => {
        unMockLoadImage();
      });

      const tearUp = async (orientation: number) => {
        mockLoadImage(shortSide, longSide, orientation);
        const imageInfo = await initialiseImagePreview(
          mockFileInfo,
          containerRect,
          defaultProps.maxZoom,
        );
        if (imageInfo !== null) {
          const { width: imageWidth, height: imageHeight } = imageInfo;
          return { imageWidth, imageHeight };
        }
        throw new Error();
      };

      it('orientation 1', async done => {
        [1, 2, 3, 4].forEach(async orientation => {
          const { imageWidth, imageHeight } = await tearUp(orientation);
          expect(imageWidth).toBeLessThan(imageHeight);
        });
        done();
      });

      it('orientation > 5', async done => {
        [5, 6, 7, 8].forEach(async orientation => {
          const { imageWidth, imageHeight } = await tearUp(orientation);
          expect(imageWidth).toBeGreaterThan(imageHeight);
        });
        done();
      });
    });
  }


() => {
      unMockLoadImage();
    }


async () => {
      mockLoadImageError();

      const imageInfo = await initialiseImagePreview(
        mockFileInfo,
        containerRect,
        defaultProps.maxZoom,
      );
      expect(imageInfo).toBeNull();
    }


async done => {
      mockLoadImage(extraLargeSize, extraLargeSize);

      const imageInfo = await initialiseImagePreview(
        mockFileInfo,
        containerRect,
        defaultProps.maxZoom,
      );
      if (imageInfo !== null) {
        expect(imageInfo.width).toEqual(
          defaultProps.containerWidth * defaultProps.maxZoom,
        );
        expect(imageInfo.height).toEqual(
          defaultProps.containerHeight * defaultProps.maxZoom,
        );
        done();
      }
    }


() => {
      const shortSide = mediumSize;
      const longSide = largeSize;

      afterEach(() => {
        unMockLoadImage();
      });

      const tearUp = async (orientation: number) => {
        mockLoadImage(shortSide, longSide, orientation);
        const imageInfo = await initialiseImagePreview(
          mockFileInfo,
          containerRect,
          defaultProps.maxZoom,
        );
        if (imageInfo !== null) {
          const { width: imageWidth, height: imageHeight } = imageInfo;
          return { imageWidth, imageHeight };
        }
        throw new Error();
      };

      it('orientation 1', async done => {
        [1, 2, 3, 4].forEach(async orientation => {
          const { imageWidth, imageHeight } = await tearUp(orientation);
          expect(imageWidth).toBeLessThan(imageHeight);
        });
        done();
      });

      it('orientation > 5', async done => {
        [5, 6, 7, 8].forEach(async orientation => {
          const { imageWidth, imageHeight } = await tearUp(orientation);
          expect(imageWidth).toBeGreaterThan(imageHeight);
        });
        done();
      });
    }


() => {
        unMockLoadImage();
      }


async (orientation: number) => {
        mockLoadImage(shortSide, longSide, orientation);
        const imageInfo = await initialiseImagePreview(
          mockFileInfo,
          containerRect,
          defaultProps.maxZoom,
        );
        if (imageInfo !== null) {
          const { width: imageWidth, height: imageHeight } = imageInfo;
          return { imageWidth, imageHeight };
        }
        throw new Error();
      }


async done => {
        [1, 2, 3, 4].forEach(async orientation => {
          const { imageWidth, imageHeight } = await tearUp(orientation);
          expect(imageWidth).toBeLessThan(imageHeight);
        });
        done();
      }


async orientation => {
          const { imageWidth, imageHeight } = await tearUp(orientation);
          expect(imageWidth).toBeLessThan(imageHeight);
        }


async done => {
        [5, 6, 7, 8].forEach(async orientation => {
          const { imageWidth, imageHeight } = await tearUp(orientation);
          expect(imageWidth).toBeGreaterThan(imageHeight);
        });
        done();
      }


async orientation => {
          const { imageWidth, imageHeight } = await tearUp(orientation);
          expect(imageWidth).toBeGreaterThan(imageHeight);
        }


() => {
    it('should set dragOrigin state with current origin when drag starts', () => {
      const { instance } = setup();
      instance.state.originX = 5;
      instance.state.originY = 10;
      instance.onDragStart();
      const dragOrigin = instance.state.dragOrigin as Vector2;
      expect(dragOrigin.x).toEqual(5);
      expect(dragOrigin.y).toEqual(10);
    });

    it('should move origin and apply constraints when dragged', () => {
      const { instance } = setup();
      jest.spyOn(instance, 'applyConstraints');
      instance.onDragStart();
      instance.onDragMove({ x: 5, y: 10 } as Vector2);
      const { originX, originY } = instance.state;
      expect(originX).toBe(5);
      expect(originY).toBe(10);
    });
  }


() => {
      const { instance } = setup();
      instance.state.originX = 5;
      instance.state.originY = 10;
      instance.onDragStart();
      const dragOrigin = instance.state.dragOrigin as Vector2;
      expect(dragOrigin.x).toEqual(5);
      expect(dragOrigin.y).toEqual(10);
    }


() => {
      const { instance } = setup();
      jest.spyOn(instance, 'applyConstraints');
      instance.onDragStart();
      instance.onDragMove({ x: 5, y: 10 } as Vector2);
      const { originX, originY } = instance.state;
      expect(originX).toBe(5);
      expect(originY).toBe(10);
    }


() => {
    it('should set clamped zoom between 0 - 1 from wheel event value', () => {
      const { instance } = setup();
      const setZoom = jest.fn();
      instance.setZoom = setZoom;
      instance.onWheel(10);
      expect(setZoom).toHaveBeenCalledWith(0.1);
      instance.onWheel(100);
      expect(setZoom).toHaveBeenCalledWith(1);
      instance.onWheel(-2000);
      expect(setZoom).toHaveBeenCalledWith(0);
    });

    it('should call onZoomChange during wheel event', done => {
      const { instance } = setup({
        ...defaultProps,
        onZoomChange(zoom: number) {
          expect(zoom).toBe(0.1);
          done();
        },
      });
      instance.onWheel(10);
    });
  }


() => {
      const { instance } = setup();
      const setZoom = jest.fn();
      instance.setZoom = setZoom;
      instance.onWheel(10);
      expect(setZoom).toHaveBeenCalledWith(0.1);
      instance.onWheel(100);
      expect(setZoom).toHaveBeenCalledWith(1);
      instance.onWheel(-2000);
      expect(setZoom).toHaveBeenCalledWith(0);
    }


done => {
      const { instance } = setup({
        ...defaultProps,
        onZoomChange(zoom: number) {
          expect(zoom).toBe(0.1);
          done();
        },
      });
      instance.onWheel(10);
    }


() => {
    it('should not render image when error', () => {
      const { wrapper, instance } = setup();
      instance.onImageError('some-error');
      wrapper.update();
      const image = wrapper.find(ImagePlacerImage);
      expect(image).toHaveLength(0);
    });

    it('should render error message if error and no errorRender passed', () => {
      const { wrapper, instance } = setup();
      instance.onImageError('some-error');
      wrapper.update();
      const errorWrapper = wrapper.find(ImagePlacerErrorWrapper);
      expect(errorWrapper).toHaveLength(1);
      expect(errorWrapper.dive().text()).toEqual('some-error');
    });

    it('should render onRenderError if error and passed', () => {
      const { wrapper, instance } = setup({
        ...defaultProps,
        onRenderError: (errorMessage: string) => <h1>{errorMessage}</h1>,
      });
      instance.onImageError('some-error');
      wrapper.update();
      const customErrorWrapper = wrapper.find('h1');
      expect(customErrorWrapper.text()).toEqual('some-error');
    });

    it('should listen to image events', () => {
      const { wrapper, instance } = setup();
      /* we need to rebind the mocked handles, so switch state around first... */
      instance.onImageError('some-error');
      wrapper.update();
      /* now re-render the container and image, to bind handles to mocked ones */
      jest.spyOn(instance, 'onImageLoad');
      jest.spyOn(instance, 'onImageError');
      wrapper.setState({ errorMessage: undefined });
      wrapper.update();
      /* now we can simluate load event and hand correct functions referenced by children */
      const image = wrapper.find(ImagePlacerImage);
      image.simulate('load');
      image.simulate('error');
      expect(instance.onImageLoad).toHaveBeenCalled();
      expect(instance.onImageError).toHaveBeenCalled();
    });

    it('should listen to container events', () => {
      /* same as above, we need to re-render after mocking functions to update child refs */
      const { wrapper, instance } = setup();
      instance.onImageError('some-error');
      wrapper.update();
      jest.spyOn(instance, 'onDragStart');
      jest.spyOn(instance, 'onDragMove');
      wrapper.setState({ errorMessage: undefined });
      wrapper.update();
      const container = wrapper.find(ImagePlacerContainer).get(0);
      container.props.onDragStart({});
      container.props.onDragMove({});
      expect(instance.onDragStart).toHaveBeenCalled();
      expect(instance.onDragMove).toHaveBeenCalled();
    });
  }


() => {
      const { wrapper, instance } = setup();
      instance.onImageError('some-error');
      wrapper.update();
      const image = wrapper.find(ImagePlacerImage);
      expect(image).toHaveLength(0);
    }


() => {
      const { wrapper, instance } = setup();
      instance.onImageError('some-error');
      wrapper.update();
      const errorWrapper = wrapper.find(ImagePlacerErrorWrapper);
      expect(errorWrapper).toHaveLength(1);
      expect(errorWrapper.dive().text()).toEqual('some-error');
    }


() => {
      const { wrapper, instance } = setup({
        ...defaultProps,
        onRenderError: (errorMessage: string) => <h1>{errorMessage}</h1>,
      });
      instance.onImageError('some-error');
      wrapper.update();
      const customErrorWrapper = wrapper.find('h1');
      expect(customErrorWrapper.text()).toEqual('some-error');
    }


(errorMessage: string) => <h1>{errorMessage}</h1>,


() => {
      const { wrapper, instance } = setup();
      /* we need to rebind the mocked handles, so switch state around first... */
      instance.onImageError('some-error');
      wrapper.update();
      /* now re-render the container and image, to bind handles to mocked ones */
      jest.spyOn(instance, 'onImageLoad');
      jest.spyOn(instance, 'onImageError');
      wrapper.setState({ errorMessage: undefined });
      wrapper.update();
      /* now we can simluate load event and hand correct functions referenced by children */
      const image = wrapper.find(ImagePlacerImage);
      image.simulate('load');
      image.simulate('error');
      expect(instance.onImageLoad).toHaveBeenCalled();
      expect(instance.onImageError).toHaveBeenCalled();
    }


() => {
      /* same as above, we need to re-render after mocking functions to update child refs */
      const { wrapper, instance } = setup();
      instance.onImageError('some-error');
      wrapper.update();
      jest.spyOn(instance, 'onDragStart');
      jest.spyOn(instance, 'onDragMove');
      wrapper.setState({ errorMessage: undefined });
      wrapper.update();
      const container = wrapper.find(ImagePlacerContainer).get(0);
      container.props.onDragStart({});
      container.props.onDragMove({});
      expect(instance.onDragStart).toHaveBeenCalled();
      expect(instance.onDragMove).toHaveBeenCalled();
    }


interface SetupInfo {
  wrapper: ShallowWrapper;
  instance: ImagePlacer;
  onDragStart: () => void;
  onDragMove: () => void;
  onWheel: () => void;
  onZoomChange: () => void;
  onSaveImage: () => void;
}


onZoomChange(zoom: number) {
          expect(zoom).toBe(0.1);
          done();
        }


export interface PayloadGeneric {
    sender: User;
}


export interface ICommitComment extends PayloadGeneric {
    action: string;
    comment: Comment;
    repository: Repository;
}


export interface ICreate extends PayloadGeneric {
    ref: string;
    ref_type: string;
    master_branch: string;
    description: string;
    pusher_type: string;
    repository: Repository;
}


export interface IDelete extends PayloadGeneric {
    ref: string;
    ref_type: string;
    pusher_type: string;
    repository: Repository;
}


export interface IDeployment extends PayloadGeneric {
    deployment: Deployment;
    repository: Repository;
}


export interface IDeployment_Status extends PayloadGeneric {
    deployment_status: {
        url: string;
        id: number;
        state: string;
        creator: User;
        description: string | null;
        target_url: string | null;
        created_at: string;
        updated_at: string;
        statuses_url: string;
        repository_url: string;
    };
    deployment: Deployment;
    repository: Repository;
}


export interface IForK extends PayloadGeneric {
    forkee: Repository;
    repository: Repository;
}


export interface IGollum extends PayloadGeneric {
    pages: {
        page_name: string;
        title: string;
        summary: string | null;
        action: string;
        sha: string;
        html_url: string;
    }[];
    repository: Repository;
}


export interface IInstallation extends PayloadGeneric {
    action: string;
    installation: Installation;
    sender: User;
}


export interface IInstallationRepository {
    action: string;
    installation: Installation;
    repository_selection: string;
    repositories_added: {
        id: number;
        name: string;
        full_name: string;
    }[];
    repositories_removed: {
        id: number;
        name: string;
        full_name: string;
    }[];
    sender: User;
    repository: Repository;
}


export interface IIssueComment extends PayloadGeneric {
    action: string;
    issue: Issue;
    comment: Comment;
    repository: Repository;
}


export interface IIssue extends PayloadGeneric {
    action: string;
    issue: Issue;
    repository: Repository;
}


export interface ILabel extends PayloadGeneric {
    action: string;
    label: Label;
    organization: Organization;
    repository: Repository;
}


export interface IMember extends PayloadGeneric {
    action: string;
    member: User;
    repository: Repository;
}


export interface IMembership extends PayloadGeneric {
    action: string;
    scope: string;
    member: User;
    team: {
        name: string;
        id: number;
        slug: string;
        permission: string;
        url: string;
        members_url: string;
        repositories_url: string;
    };
    organization: Organization;
}


export interface IMilestone extends PayloadGeneric {
    action: string;
    milestone: Milestone;
    organization: Organization;
    repository: Repository;
}


export interface IOrganization extends PayloadGeneric {
    action: string;
    invitation: {
        id: number;
        login: string;
        email: string | null;
        role: string;
    };
    membership: {
        url: string;
        state: string;
        role: string;
        organization_url: string;
        user: User;
    };
    organization: Organization;
}


export interface IOrgBlock extends PayloadGeneric {
    action: string;
    blocked_user: User;
    organization: Organization;
}


export interface IPageBuild extends PayloadGeneric {
    id: number;
    build: {
        url: string;
        status: string;
        error?: {
            message: string | null;
        };
        pusher: User;
        commit: string;
        duration: number;
        created_at: string;
        updated_at: string;
    };
    repository: Repository;
}


export interface IPullRequest extends PayloadGeneric {
    action: string;
    number: number;
    pull_request: PullRequest;
    installation: {
        id: number;
    };
    repository: Repository;
}


export interface IPullRequestReview extends PayloadGeneric {
    action: string;
    review: {
        id: number;
        user: User;
        body: string;
        submitted_at: string;
        state: string;
        html_url: string;
        pull_request_url: string;
    };
    pull_request: PullRequest;
    repository: Repository;
}


export interface IPullRequestReviewComment extends PayloadGeneric {
    action: string;
    comment: Comment;
    pull_request: PullRequest;
    repository: Repository;
}


export interface IPush extends PayloadGeneric {
    ref: string;
    before: string;
    after: string;
    created: boolean;
    deleted: boolean;
    forced: boolean;
    base_ref: null | string;
    compare: string;
    commits: Commit[];
    head_commit: Commit;
    pusher: {
        name: string;
        email: string;
    };
    repository: Repository;
}


export interface IRelease extends PayloadGeneric {
    action: string;
    release: Release;
    repository: Repository;
}


export interface IRepository extends PayloadGeneric {
    action: string;
    organization: Organization;
    repository: Repository;
}


export interface IStatus extends PayloadGeneric {
    id: number;
    sha: string;
    name: string;
    target_url: string;
    context: string;
    description: string | null;
    state: string;
    commit: {
        sha: string;
        commit: Commit;
        url: string;
        html_url: string;
        comments_url: string;
        author: User;
        committer: User;
        parents: any[];
    };
    branches: Branch[];
    created_at: string;
    updated_at: string;
    repository: Repository;
}


export interface IWatch extends PayloadGeneric {
    action: string;
    repository: Repository;
}


/**
 * loop amend
 *
 * @param {string} payload
 * @param {*} collect
 */
function loopAmend(payload: string, collect: ICollectItem[]) {
  collect.forEach(item => {
    const { routePath, component, children } = item;

    // reduce routePath to the nested
    item.routePathAmend = routePath;

    if (payload && payload !== '/') {
      item.routePathAmend = routePath.replace(payload, '');
    }
    // tansform component name
    item.component = transformComponentName(component);

    if (children) {
      loopAmend(routePath, children);
    }
  });
}


/**
 * add default layout
 * default layout: `src/layouts/index.[jsx|tsx]`
 */
function addDefaultLayout(
  rootDir: string,
  routersTempPath: string,
  collect: ICollectItem[]
) {
  const rootRoute = collect && collect[0];
  // get the js like file
  const layoutJSFilePaths = getJsFilePathsIn(
    path.join(rootDir, 'src/layouts/index')
  );
  const defaultLayoutExists = Boolean(layoutJSFilePaths.length);
  const defaultLayoutPath = layoutJSFilePaths[0] || '';
  const defaultLayoutPathRelative = formatPathForWin(
    path.relative(
      path.dirname(routersTempPath),
      defaultLayoutPath
    )
  );

  if (defaultLayoutExists) {
    // has layout nested such as _layout
    if (rootRoute && rootRoute.routePath === '/') {
      rootRoute.filePath = defaultLayoutPathRelative;
    } else {
      const spliceRoutes = collect.splice(0, collect.length);
      collect.push({
        routePath: '/',
        component: 'Layout',
        filePath: defaultLayoutPathRelative,
        isLayoutLike: true,
        children: spliceRoutes
      });

    }
  }
}


/**
 * add default 404
 * default file: `src/pages/404?[/index].[jsx|tsx]`
 */
function addDefault404(
  rootDir: string,
  routersTempPath: string,
  collect: ICollectItem[]
) {
  const rootRoute = collect && collect[0];
  // get the js like file
  const defaultMatch = path.join(rootDir, 'src/pages/404');
  const extMatch = '.@(ts?(x)|js?(x))';
  const page404JSFilePaths = glob.sync(`${defaultMatch}{/index${extMatch},${extMatch}}`);
  const default404Exists = Boolean(page404JSFilePaths.length);
  const default404Path = page404JSFilePaths[0] || '';
  const default404PathRelative = formatPathForWin(
    path.relative(
      path.dirname(routersTempPath),
      default404Path
    )
  );

  if (default404Exists) {
    const config404 = {
      routePath: '',
      exact: String(false),
      component: 'Page404',
      filePath: default404PathRelative,
      isLayoutLike: false
    };
    if (rootRoute && rootRoute.routePath === '/') {
      // has layout nested such as _layout
      if (!rootRoute.children) rootRoute.children = [];
      rootRoute.children.push(config404);
    } else if (collect) {
      collect.push(config404);
    }
  }
}


export default function amender(
  rootDir: string,
  routersTempPath: string,
  routesCollect: ICollectItem[]
) {
  addDefaultLayout(rootDir, routersTempPath, routesCollect);
  addDefault404(rootDir, routersTempPath, routesCollect);
  loopAmend('', routesCollect);
}


item => {
    const { routePath, component, children } = item;

    // reduce routePath to the nested
    item.routePathAmend = routePath;

    if (payload && payload !== '/') {
      item.routePathAmend = routePath.replace(payload, '');
    }
    // tansform component name
    item.component = transformComponentName(component);

    if (children) {
      loopAmend(routePath, children);
    }
  }


function createUniqueId() {
  const random = () => Number(Math.random().toString().substr(2)).toString(36)
  const arr = [String(Date.now())];
  function createId() {
      var num = random();
      arr.push(num)
  }
  var i = 0;
  while(i < 4) {
      createId();
      i++;
  }
  return hexMD5(arr.join(','));
}


function createId() {
      var num = random();
      arr.push(num)
  }


() => Number(Math.random().toString().substr(2)).toString(36)


(filename: string) => /\.module\.\w+$/i.test(filename)


(loaderContext: any, localIdentName: string, localName: string, options: any) => {
              // 配合PostcssPreSelector插件
              if (localName === this.buildId) {
                return localName;
              }

              if (!options.context) {
                options.context = loaderContext.rootContext;
              }

              const request = path
                .relative(options.context, loaderContext.resourcePath)
                .replace(/\\/g, '/');

              options.content = `${options.hashPrefix + request}+${localName}`;

              localIdentName = localIdentName.replace(/\[local\]/gi, localName);

              const hash = loaderUtils.interpolateName(
                loaderContext,
                localIdentName,
                options
              );

              return hash
            }


interface ConfigHelperOptions {
  dev: boolean;
  port: string;
}


readAllConfig() {
    this.projectConfig = { ...DEFAULT_PROJECT_CONFIG, ...this._readConfig<ProjectConfig>('mona.config') };
    this.appConfig = { ...DEFAULT_APP_CONFIG, ...this._readConfig<AppConfig>('app.config') };
    this.entryPath = searchScriptFile(path.resolve(this.cwd, this.projectConfig.input));
    if (this.options.port) {
      this.projectConfig.dev = { ...this.projectConfig.dev, port: this.options.port };
    }
  }


generate() {
    const config: Configuration = {
      mode: this._createMode(),
      devtool: this.options.dev ? 'cheap-source-map' : undefined,
      entry: this._createEntry(),
      output: this._createOutput(),
      resolve: this._createResolve(),
      module: this._createModule(),
      plugins: this._createPlugins(),
      optimization: this._createOptimization() as any,
    };

    const raw = this.projectConfig.raw;
    return raw ? raw(config) : config;
  }


private _createOptimization() {
    if (this.options.dev) return {};
    return {
      minimize: true,
      minimizer: [new TerserWebpackPlugin({ parallel: true, extractComments: false }), new CssMiniminzerPlugin()],
      splitChunks: {
        chunks: 'async',
        minSize: 20000,
        minRemainingSize: 0,
        minChunks: 1,
        maxAsyncRequests: 30,
        maxInitialRequests: 30,
        enforceSizeThreshold: 50000,
        cacheGroups: {
          reactBase: {
            name: 'react-chunk',
            test: /react/,
            chunks: 'initial',
            priority: 10,
          },
          common: {
            name: 'common',
            chunks: 'initial',
            priority: 2,
            minChunks: 2,
          },
          default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
          }
        }
      }
    }
  }


private _createResolve() {
    return {
      extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
      alias: {
        '@': path.resolve(this.cwd, './src'),
      },
    };
  }


private _readConfig<T>(configName: string): T {
    const projectConfigPath = path.join(this.cwd, configName);
    const fullConfigPath = searchScriptFile(projectConfigPath);
    if (fs.existsSync(fullConfigPath)) {
      const projectConfig = readConfig<T>(fullConfigPath);
      return projectConfig;
    } else {
      throw new Error('无效的项目目录，请在mona项目根目录执行命令')
    }
  }


private _createEntry() {
     return path.join(this.entryPath, '..', 'app.entry.js')
  }


private _createMode() {
    return this.options.dev ? 'development' : 'production';
  }


private _createOutput() {
    return {
      path: path.join(this.cwd, this.projectConfig.output),
      filename: '[name].[contenthash:7].js',
      publicPath: '/',
      libraryTarget: 'umd',
      globalObject: 'window',
      chunkLoadingGlobal: `webpackJsonp_${this.projectConfig.projectName}_${Date.now()}`,
    };
  }


private _createModule() {
    const module: webpack.ModuleOptions = {
      rules: this._createModuleRules(),
    };

    return module;
  }


private _createModuleRules() {
    const rules: RuleSetRule[] = [];

    // handle script
    rules.push({
      test: /\.((j|t)sx?)$/i,
      use: [
        {
          loader: require.resolve('babel-loader'),
          options: {
            babelrc: false,
            presets: [
              [require.resolve('@babel/preset-env')],
              [require.resolve('@babel/preset-typescript')],
              [require.resolve('@babel/preset-react')],
            ],
            plugins: [
              this.options.dev && require.resolve('react-refresh/babel')
            ].filter(Boolean)
          },
        },
      ],
    });

    const styleLoader = [
      {
        loader: require.resolve('css-loader'),
        options: {
          modules: {
            auto: (filename: string) => /\.module\.\w+$/i.test(filename),
            localIdentName: '[local]___[hash:base64:5]',
            getLocalIdent: (loaderContext: any, localIdentName: string, localName: string, options: any) => {
              // 配合PostcssPreSelector插件
              if (localName === this.buildId) {
                return localName;
              }

              if (!options.context) {
                options.context = loaderContext.rootContext;
              }

              const request = path
                .relative(options.context, loaderContext.resourcePath)
                .replace(/\\/g, '/');

              options.content = `${options.hashPrefix + request}+${localName}`;

              localIdentName = localIdentName.replace(/\[local\]/gi, localName);

              const hash = loaderUtils.interpolateName(
                loaderContext,
                localIdentName,
                options
              );

              return hash
            }
          },
        },
      },
      {
        loader: require.resolve('postcss-loader'),
        options: {
          postcssOptions: {
            plugins: [
              require.resolve('postcss-import'),
              [path.join(__dirname, './plugins/PostcssPreSelector.js'), { selector: `#${this.buildId}` }]
            ]
          }
        }
      },
      require.resolve('less-loader'),
    ]
    if (!this.options.dev) {
      styleLoader.unshift(MiniCssExtractPlugin.loader)
    } else {
      styleLoader.unshift(require.resolve('style-loader'))
    }

    // handle style
    rules.push({
      test: /\.(c|le)ss$/i,
      use: styleLoader,
    });

    // handle assets
    rules.push({
      test: /\.(png|jpe?g|gif|webp)$/i,
      type: 'asset/resource',
    });
    rules.push({
      test: /\.svg$/i,
      use: [
        { loader: require.resolve('@svgr/webpack') },
      ]
    });
    rules.push({
      test: /\.(ttf|eot|woff|woff2)$/i,
      type: 'asset/resource',
    });

    return rules;
  }


private _createPlugins() {
    // const EntryMoudleInstance = this.entryModule.module;
    let plugins = [
      // EntryMoudleInstance,
      new ConfigHMRPlugin(this),
      new MiniCssExtractPlugin({
        filename: '[name].[contenthash:7].css'
      }),
      // EntryMoudleInstance,
      new HtmlWebpackPlugin({
        templateContent: `

          <!DOCTYPE html>

              <meta charset="utf-8">
              <title>Mona Plugin</title>
              <meta name="viewport" content="width=device-width, initial-scale=1">

        `,
        minify: {
          collapseWhitespace: true,
          keepClosingSlash: true,
          removeComments: false,
          removeRedundantAttributes: true,
          removeScriptTypeAttributes: true,
          removeStyleLinkTypeAttributes: true,
          useShortDoctype: true
        }
      }),
    ]

    if (this.options.dev) {
      plugins = [
        new ReactRefreshWebpackPlugin(),
        ...plugins
      ]
    } else {
      plugins = [
        new MiniCssExtractPlugin({
          filename: '[name].[contenthash:7].css'
        }),
        ...plugins
      ]
    }
    return plugins;
  }


(args) => html`
    <wcs-textarea icon=${args.icon}
                  ?auto-grow="${args.autoGrow}"
                  ?disabled="${args.disabled}"
                  placeholder=${args.placeholder}
                  value=${args.value}>
    </wcs-textarea>
`


/**
 * @classdesc
 * Vector data that is rendered client-side.
 * Note that any property set in the options is set as a {@link module:ol/Object~BaseObject}
 * property on the layer object; for example, setting `title: 'My Title'` in the
 * options means that `title` is observable, and has get/set accessors.
 *
 * @template {import("../source/Vector.js").default} VectorSourceType
 * @extends {BaseVectorLayer<VectorSourceType, CanvasVectorLayerRenderer>}
 * @api
 */
declare class VectorLayer<VectorSourceType extends import("../source/Vector.js").default<any>> extends BaseVectorLayer<VectorSourceType, CanvasVectorLayerRenderer> {
    /**
     * @param {import("./BaseVector.js").Options<VectorSourceType>} [opt_options] Options.
     */
    constructor(opt_options?: import("./BaseVector.js").Options<VectorSourceType> | undefined);
    createRenderer(): any;
}


createRenderer(): any;


(result) => {
        const [parameterNode] = tsquery(result, COMPONENT_PARAMETERS_PROPERTY_VALUE_QUERY);
        if (!parameterNode) {
          return [];
        }
        const [renderOptionsNode] = tsquery(ast, RENDER_OPTIONS_QUERY);

        const renderOptionsText = renderOptionsNode.getFullText();
        const bracketIndex = renderOptionsText.indexOf('{');
        const renderOptions =
          renderOptionsText.substring(0, bracketIndex + 1) +
          `componentProperties:${parameterNode.getFullText()},` +
          renderOptionsText.substr(bracketIndex + 1);

        const replacement = new Replacement(renderOptionsNode.getStart(), renderOptionsNode.getWidth(), renderOptions);
        const start = renderOptionsNode.getStart();
        const end = renderOptionsNode.getEnd();

        const replacementOriginal = new Replacement(parameterNode.getStart(), parameterNode.getWidth(), '');
        const startOriginal = renderOptionsNode.getStart();
        const endOriginal = renderOptionsNode.getEnd();

        return [
          new RuleFailure(ast, startOriginal, endOriginal, FAILURE_MESSAGE, this.ruleName, replacementOriginal),
          new RuleFailure(ast, start, end, FAILURE_MESSAGE, this.ruleName, replacement),
        ];
      }


(rules, rule) => rules.concat(rule)


export class Rule extends Rules.AbstractRule {
  public apply(ast: ts.SourceFile): Array<RuleFailure> {
    return tsquery(ast, IS_COMPONENT_PROPERTY_QUERY)
      .map((result) => {
        const [parameterNode] = tsquery(result, COMPONENT_PARAMETERS_PROPERTY_VALUE_QUERY);
        if (!parameterNode) {
          return [];
        }
        const [renderOptionsNode] = tsquery(ast, RENDER_OPTIONS_QUERY);

        const renderOptionsText = renderOptionsNode.getFullText();
        const bracketIndex = renderOptionsText.indexOf('{');
        const renderOptions =
          renderOptionsText.substring(0, bracketIndex + 1) +
          `componentProperties:${parameterNode.getFullText()},` +
          renderOptionsText.substr(bracketIndex + 1);

        const replacement = new Replacement(renderOptionsNode.getStart(), renderOptionsNode.getWidth(), renderOptions);
        const start = renderOptionsNode.getStart();
        const end = renderOptionsNode.getEnd();

        const replacementOriginal = new Replacement(parameterNode.getStart(), parameterNode.getWidth(), '');
        const startOriginal = renderOptionsNode.getStart();
        const endOriginal = renderOptionsNode.getEnd();

        return [
          new RuleFailure(ast, startOriginal, endOriginal, FAILURE_MESSAGE, this.ruleName, replacementOriginal),
          new RuleFailure(ast, start, end, FAILURE_MESSAGE, this.ruleName, replacement),
        ];
      })
      .reduce((rules, rule) => rules.concat(rule), []);
  }
}


public apply(ast: ts.SourceFile): Array<RuleFailure> {
    return tsquery(ast, IS_COMPONENT_PROPERTY_QUERY)
      .map((result) => {
        const [parameterNode] = tsquery(result, COMPONENT_PARAMETERS_PROPERTY_VALUE_QUERY);
        if (!parameterNode) {
          return [];
        }
        const [renderOptionsNode] = tsquery(ast, RENDER_OPTIONS_QUERY);

        const renderOptionsText = renderOptionsNode.getFullText();
        const bracketIndex = renderOptionsText.indexOf('{');
        const renderOptions =
          renderOptionsText.substring(0, bracketIndex + 1) +
          `componentProperties:${parameterNode.getFullText()},` +
          renderOptionsText.substr(bracketIndex + 1);

        const replacement = new Replacement(renderOptionsNode.getStart(), renderOptionsNode.getWidth(), renderOptions);
        const start = renderOptionsNode.getStart();
        const end = renderOptionsNode.getEnd();

        const replacementOriginal = new Replacement(parameterNode.getStart(), parameterNode.getWidth(), '');
        const startOriginal = renderOptionsNode.getStart();
        const endOriginal = renderOptionsNode.getEnd();

        return [
          new RuleFailure(ast, startOriginal, endOriginal, FAILURE_MESSAGE, this.ruleName, replacementOriginal),
          new RuleFailure(ast, start, end, FAILURE_MESSAGE, this.ruleName, replacement),
        ];
      })
      .reduce((rules, rule) => rules.concat(rule), []);
  }


export function OrgSwitcher({ onDismiss }: Props): ReactElement {
  const { value: orgs = [] } = useAsync(() => {
    return api.loadOrgs();
  }, []);
  const currentOrgId = contextSrv.user.orgId;
  const contentClassName = css({
    display: 'flex',
    maxHeight: 'calc(85vh - 42px)',
  });
  const setCurrentOrg = async (org: UserOrgDTO) => {
    await api.setUserOrg(org);
    window.location.href = `${config.appSubUrl}${config.appSubUrl.endsWith('/') ? '' : '/'}?orgId=${org.orgId}`;
  };

  return (
    <Modal
      title="Switch Organization"
      icon="arrow-random"
      onDismiss={onDismiss}
      isOpen={true}
      contentClassName={contentClassName}
    >
      <CustomScrollbar autoHeightMin="100%">

              Name
              Role

            {orgs.map((org) => (

                {org.name}
                {org.role}

                  {org.orgId === currentOrgId ? (
                    <Button size="sm">Current</Button>
                  ) : (
                    <Button variant="secondary" size="sm" onClick={() => setCurrentOrg(org)}>
                      Switch to
                    </Button>
                  )}


() => {
    return api.loadOrgs();
  }


async (org: UserOrgDTO) => {
    await api.setUserOrg(org);
    window.location.href = `${config.appSubUrl}${config.appSubUrl.endsWith('/') ? '' : '/'}?orgId=${org.orgId}`;
  }


(org) => (

                {org.name}
                {org.role}
                <td


interface Props {
  onDismiss: () => void;
}


() => {
  // If we are running with the mobile env flag set, short-circuit to `true`
  let check = false
  ;(function(a) {
    if (
      /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
        a
      ) ||
      /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
        a.substr(0, 4)
      )
    )
      check = true
    // @ts-ignore
  })(navigator.userAgent || navigator.vendor || window.opera)
  // iPad iOS 13
  if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
    check = true
  return check
}


({
  desktopStyles,
  mobileStyles
}: {
  desktopStyles: Styles
  mobileStyles: Styles
}) => ({
  ...desktopStyles,
  ...(isMobile() && mobileStyles)
})


type Styles = { readonly [key: string]: string }


(event: any) => {
      // tslint:disable-next-line:no-unused-expression
      new qq.maps.Marker({
        position: event.latLng,
        map: this.map,
      });
      this.zone.run(() => {
        this.status = `click ${+new Date()}`;
      });
    }


() => {
        this.status = `click ${+new Date()}`;
      }


(eventName) => {
      qq.maps.event.clearListeners(this.map, eventName);
    }


@Component({
  selector: 'demo',
  templateUrl: './demo.component.html',
  styleUrls: ['./demo.component.scss'],
  encapsulation: ViewEncapsulation.None,
})
export class DemoComponent implements OnDestroy {
  constructor(private el: ElementRef, private zone: NgZone) {}
  options: any = {};
  status = '';
  @ViewChild('map') mapComp!: AqmComponent;

  private map: any;

  // 卫星
  satelliteOptions: any;
  private mapSatellite: any;
  onReady(mapNative: any): void {
    mapNative.setOptions({
      zoom: 12,
      center: new qq.maps.LatLng(39.916527, 116.397128),
    });
    this.map = mapNative;
    this.status = '加载完成';
    // 添加监听事件
    qq.maps.event.addListener(this.map, 'click', (event: any) => {
      // tslint:disable-next-line:no-unused-expression
      new qq.maps.Marker({
        position: event.latLng,
        map: this.map,
      });
      this.zone.run(() => {
        this.status = `click ${+new Date()}`;
      });
    });
  }

  panTo(): void {
    this.map.panTo(new qq.maps.LatLng(39.9, 116.4));
  }

  zoom(): void {
    this.map.zoomTo((this.map.getZoom() + 1) % 17);
  }

  infoWindow(): void {
    const infoWin = new qq.maps.InfoWindow({
      map: this.map,
    });
    infoWin.open();
    infoWin.setContent('Hello world');
    infoWin.setPosition(this.map.getCenter());
  }
  onReadySatellite(mapNative: any): void {
    mapNative.setOptions({
      zoom: 14,
      center: new qq.maps.LatLng(39.916527, 116.397128),
      mapTypeId: qq.maps.MapTypeId.SATELLITE,
    });
    this.mapSatellite = mapNative;
  }

  ngOnDestroy(): void {
    ['click'].forEach((eventName) => {
      qq.maps.event.clearListeners(this.map, eventName);
    });
  }
}


onReady(mapNative: any): void {
    mapNative.setOptions({
      zoom: 12,
      center: new qq.maps.LatLng(39.916527, 116.397128),
    });
    this.map = mapNative;
    this.status = '加载完成';
    // 添加监听事件
    qq.maps.event.addListener(this.map, 'click', (event: any) => {
      // tslint:disable-next-line:no-unused-expression
      new qq.maps.Marker({
        position: event.latLng,
        map: this.map,
      });
      this.zone.run(() => {
        this.status = `click ${+new Date()}`;
      });
    });
  }


panTo(): void {
    this.map.panTo(new qq.maps.LatLng(39.9, 116.4));
  }


zoom(): void {
    this.map.zoomTo((this.map.getZoom() + 1) % 17);
  }


infoWindow(): void {
    const infoWin = new qq.maps.InfoWindow({
      map: this.map,
    });
    infoWin.open();
    infoWin.setContent('Hello world');
    infoWin.setPosition(this.map.getCenter());
  }


onReadySatellite(mapNative: any): void {
    mapNative.setOptions({
      zoom: 14,
      center: new qq.maps.LatLng(39.916527, 116.397128),
      mapTypeId: qq.maps.MapTypeId.SATELLITE,
    });
    this.mapSatellite = mapNative;
  }


ngOnDestroy(): void {
    ['click'].forEach((eventName) => {
      qq.maps.event.clearListeners(this.map, eventName);
    });
  }


export function schemaProviderFromConfig(
  config: ApolloConfig
): GraphQLSchemaProvider {
  if (isServiceConfig(config)) {
    if (config.service.localSchemaFile) {
      return new FileSchemaProvider({ path: config.service.localSchemaFile });
    }
    if (config.service.endpoint) {
      return new IntrospectionSchemaProvider(config.service.endpoint);
    }
  }

  if (isClientConfig(config)) {
    if (typeof config.client.service === "string") {
      return new EngineSchemaProvider(config);
    }
    return new IntrospectionSchemaProvider(config.client.service);
  }
  throw new Error("No provider was created for config");
}


/**
 * @description
 * Check if the given label is listed as a label of the given issue
 *
 * @param {Readonly<Issue>} issue A GitHub issue containing some labels
 * @param {Readonly<string>} label The label to check the presence with
 *
 * @return {boolean} Return true when the given label is also in the given issue labels
 */
export function isLabeled(
  issue: Readonly<Issue>,
  label: Readonly<string>
): boolean {
  return !!issue.labels.find((issueLabel: Readonly<Label>): boolean => {
    return cleanLabel(label) === cleanLabel(issueLabel.name);
  });
}


function cleanLabel(label: Readonly<string>): CleanLabel {
  return deburr(label.toLowerCase());
}


(issueLabel: Readonly<Label>): boolean => {
    return cleanLabel(label) === cleanLabel(issueLabel.name);
  }


export interface PaletteEntry {
  label: string;
  id: string;
  treeId?: string; // totalmente opcional
  entries?: PaletteEntry[]; // deve obrigatoriamente ter APENAS ou 'entries' ou 'action'
  action?: () => void | Observable<any>; // TODO incluir comportamento com observable
}


export interface PaletteTreeEntry {
  label: string;
  id: string;
}


@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [AuthResolver, AuthService],
})
export class AuthModule {}


(siteData: any) => new Site(siteData)


/**
 * @class
 * @classdesc - A class for handling the Sites API document for recieving &
 *    sending API calls that require the Sites document.
 */
export default class Sites {
  public count: number = 0;
  public data: Site[] = [];
  public expand: string[] = [];
  public next: string = '';
  public previous: string = '';
  public select: string = '';
  public start: number = 0;
  public total: number = 0;

  /**
   * @param {Object} args - The raw JSON result for the Sites call.
   * @constructor
   */
  constructor(args: { count: number; expand: never[]; next: string; previous: string; select: string; start: number; total: number; data: any[]; }) {
    if (args) {
      this.count = args.count || 0;
      this.expand = args.expand || [];
      this.next = args.next || '';
      this.previous = args.previous || '';
      this.select = args.select || '';
      this.start = args.start || 0;
      this.total = args.total || 0;

      if (args.data && args.data.length) {
        this.data = args.data.map((siteData: any) => new Site(siteData));
      }
    }
  }
}


export default function generator (test: string[][], options: GeneratorOptions): GeneratorMatch {
  const mnemonic = options.withHex
    ? undefined
    : mnemonicGenerate(12);
  const seed = mnemonic
    ? mnemonicToMiniSecret(mnemonic)
    : randomAsU8a();
  const pair = options.type === 'sr25519'
    ? sr25519PairFromSeed(seed)
    : ed25519PairFromSeed(seed);
  const address = encodeAddress(pair.publicKey, options.ss58Format);
  const { count, offset } = calculate(test, address, options);

  return {
    address,
    count,
    mnemonic,
    offset,
    seed
  };
}


function updateEventStatsStdout() {
  logUpdate(
    COLORS.FG_CYAN +
      `Socket count: ${STATS_OBSERVABLE.CONNECTED_SOCKETS.size} | ` +
      COLORS.RESET +
      COLORS.FG_GREEN +
      `Data Events Received: ${STATS_OBSERVABLE.DATA_EVENT_COUNT} | ` +
      COLORS.RESET +
      COLORS.FG_RED +
      `Error Events Received: ${STATS_OBSERVABLE.ERROR_EVENT_COUNT} ` +
      COLORS.RESET
  )
}


async function assertDatabaseConnection() {
  return knexConnection.raw('select 1+1 as result').catch((err: any) => {
    console.log('Failed to establish connection to database! Exiting...')
    console.log(err)
    process.exit(1)
  })
}


function prettyPrintConfig(options) {
  console.table({
    url: options.url,
    db_connection_string: options.db_connection_string,
  })
  console.table({ headers: options.headers })
  console.table({ config: options.config }, [
    'label',
    'max_connections',
    'connections_per_second',
  ])
  console.table({ variables: options.config.variables })
}


/**
 * =====================
 *   MAIN PROGRAM CODE
 * =====================
 */
export async function main(opts: SubscriptionBenchConfig) {
  if (!opts) {
    throw new Error("Subscription options invalid");
  }
  if (!opts.db_connection_string) {
    throw new Error("DB Connection String not found");
  }

  // Open the DB connection using connection string from config file
  let dbConfig = {
    client: 'pg',
    connection: opts.db_connection_string,
    migrations: {
      directory: './src/migrations',
    },
  } as Knex.Config;
  knexConnection = Knex(dbConfig);
  Model.knex(knexConnection);

  const options: SubscriptionBenchConfig = opts;
  /**
   * Any time values change in these stats, run the below function
   * currently just updates the terminal output text with new data
   *
   * NOTE: This only works because updateEventStatsStdout() references
   * variables from the observable function, so the Proxy knows to fire
   */

  observe(() => {
    updateEventStatsStdout()
  })

  /**
   * Logging
   */

  const database =
    process.env.PG_CONNECTION_STRING || options.db_connection_string
  console.log('Asserting database connectivity, trying to conect to:')
  console.log(COLORS.FG_CYAN, database, COLORS.RESET)

  await assertDatabaseConnection()
  prettyPrintConfig(options)

  console.log(
    'Connected, starting subscriptions benchmark for a total of',
    options.config.max_connections,
    'sockets at a connection rate of',
    options.config.connections_per_second,
    'sockets per second'
  )

  /**
   * Execution
   */

  const socketManagerParams = yamlConfigToSocketManagerParams(options)
  const socketManager = new SocketManager(socketManagerParams)

  const MAX_CONNECTIONS = options.config.max_connections
  const SPAWN_RATE = 1000 / options.config.connections_per_second

  let socketSpawned = 0
  const spawnFn = () => {
    socketSpawned++
    return socketManager.spawnConnection().then((socket) => {
      if (socketSpawned >= MAX_CONNECTIONS) clearInterval(spawnInterval)
    })
  }

  const spawnInterval = setInterval(spawnFn, SPAWN_RATE)
  process.on('SIGINT', () => exit(socketManager))
}


/**
 * =====================
 * EXIT TEARDOWN PROCESS
 * =====================
 */
async function exit(socketManager: SocketManager) {
  console.log('\nExecuting Teardown Process')
  try {
    console.log('Starting to close socket connections')
    await socketManager.closeSockets()
  } catch (error) {
    console.log('Error while closing socket connections:', error)
  }

  try {
    console.log('Sockets closed, attempting to insert event data')
    const events = await socketManager.insertEvents()
    console.log(
      `Inserted total of ${events.length} events for label ${socketManager.config.label}`
    )
  } catch (error) {
    console.log('Error while inserting events:', error)
  }

  try {
    console.log('Trying to close DB connection pool')
    await knexConnection.destroy()
    console.log('Database connection destroyed')
  } catch (error) {
    console.log('Error while destroying database connection:', error)
  }

  console.log('Now exiting the process')
  process.exit(0)
}


(conn) => {
        conn.socket.sendPacked({ type: GQL.CONNECTION_TERMINATE })
        conn.socket.close()
      }


(conn) => conn.events


(url) => new WebSocket(url, 'graphql-ws', { headers })


(event) => event


(data) => JSON.stringify(data)


(data) => JSON.parse(data as string)


() => {
      // Print debug message if enabled
      if (DEBUG) console.log('Socket ID', this.props.id, 'closed')
      // Remove socket ID from Observable ES6 Set of connected sockets
      STATS_OBSERVABLE.CONNECTED_SOCKETS.delete(this.props.id)
      // If the socket is not currently trying to reconnect, begin trying
      if (!this.isReconnecting) this.startSubscription()
      // Set reconnecting state to true
      this.isReconnecting = true
    }


() => {
      // Print debug message if enabled
      if (DEBUG) console.log('Socket ID', this.props.id, 'connected')
      // Add the socket ID to ES6 Set of connected sockets
      STATS_OBSERVABLE.CONNECTED_SOCKETS.add(this.props.id)
      // Set reconnecting state to false
      this.isReconnecting = false
    }


(data) => console.log('sent', data)


(data) => {
      switch (data.type) {
        case GQL.DATA:
          const event = this.makeEventRow({ payload: data.payload, err: false })
          if (DEBUG) console.log('CALLED GQL.DATA CASE, GOT EVENT ROW', event)
          STATS_OBSERVABLE.DATA_EVENT_COUNT++
          this.events.push(event)
          break
        case GQL.ERROR:
          const error = this.makeEventRow({ payload: data.payload, err: true })
          if (DEBUG) console.log('CALLED GQL.ERROR CASE, GOT ERROR ROW', data)
          STATS_OBSERVABLE.ERROR_EVENT_COUNT++
          this.events.push(error)
          break
      }
    }


(err: any) => {
    console.log('Failed to establish connection to database! Exiting...')
    console.log(err)
    process.exit(1)
  }


() => {
    updateEventStatsStdout()
  }


() => {
    socketSpawned++
    return socketManager.spawnConnection().then((socket) => {
      if (socketSpawned >= MAX_CONNECTIONS) clearInterval(spawnInterval)
    })
  }


(socket) => {
      if (socketSpawned >= MAX_CONNECTIONS) clearInterval(spawnInterval)
    }


() => exit(socketManager)


export class SocketManager {
  private nextSocketId = 1
  public connections: { [id: number]: Connection } = {}
  public config: SockerManagerConfig
  public queryArgGenerator: Iterator<any>

  constructor(config: SockerManagerConfig) {
    this.config = config
    this.queryArgGenerator = argumentGenerator(config.variables)
  }

  public closeSockets() {
    return Promise.all(
      Object.values(this.connections).map((conn) => {
        conn.socket.sendPacked({ type: GQL.CONNECTION_TERMINATE })
        conn.socket.close()
      })
    )
  }

  public get allEvents() {
    return Object.values(this.connections).flatMap((conn) => conn.events)
  }

  public async insertEvents() {
    return Events.query()
      .allowInsert('[connection_id, event_number, event_data, event_time]')
      .insertGraph(this.allEvents)
  }

  public async spawnConnection() {
    const socketId = this.nextSocketId++
    const socketManagerConfig = this.config
    const queryVariables = this.queryArgGenerator.next().value
    try {
      const connection = new Connection({
        id: socketId,
        queryVariables,
        socketManagerConfig,
      })
      connection.startSubscription()
      this.connections[socketId] = connection
      return connection
    } catch (err) {
      console.log('Caught error when calling spawnConnection(), exiting', err)
      process.exit(1)
    }
  }
}


class Connection {
  public eventNumber = 1
  public events: Array<any> = []
  public socket: WebSocketAsPromised
  public isReconnecting: boolean = false

  constructor(public props: ConnectionParams) {
    this.socket = this.makeSocket()
    this.configureMessageHandlers()
  }

  private makeSocket() {
    const { endpoint, headers } = this.props.socketManagerConfig
    return new WebSocketAsPromised(endpoint, {
      createWebSocket: (url) => new WebSocket(url, 'graphql-ws', { headers }),
      extractMessageData: (event) => event,
      packMessage: (data) => JSON.stringify(data),
      unpackMessage: (data) => JSON.parse(data as string),
    } as any)
  }

  // TODO: Make the retry configurable through config.yaml
  @Reattempt({ times: 60, delay: 1000 })
  public async startSubscription() {
    const socket = this.socket
    const { id, queryVariables } = this.props
    const { headers, subscriptionString } = this.props.socketManagerConfig

    if (DEBUG) console.log('Socket ID', id, 'attempting to start subscription')

    await socket.open()
    socket.sendPacked({
      type: GQL.CONNECTION_INIT,
      payload: { headers },
    })
    socket.sendPacked({
      id: String(id),
      type: GQL.START,
      payload: {
        query: subscriptionString,
        variables: queryVariables,
      },
    })
  }

  private makeEventRow({ payload, err }) {
    const { label, insertPayloadData } = this.props.socketManagerConfig
    return {
      label,
      is_error: err,
      operation_id: 1,
      connection_id: this.props.id,
      event_number: this.eventNumber++,
      event_data: insertPayloadData ? payload : { data: null },
      event_time: new Date().toISOString(),
    }
  }

  private configureMessageHandlers() {
    // On socket close:
    this.socket.onClose.addListener(() => {
      // Print debug message if enabled
      if (DEBUG) console.log('Socket ID', this.props.id, 'closed')
      // Remove socket ID from Observable ES6 Set of connected sockets
      STATS_OBSERVABLE.CONNECTED_SOCKETS.delete(this.props.id)
      // If the socket is not currently trying to reconnect, begin trying
      if (!this.isReconnecting) this.startSubscription()
      // Set reconnecting state to true
      this.isReconnecting = true
    })

    // On socket open:
    this.socket.onOpen.addListener(() => {
      // Print debug message if enabled
      if (DEBUG) console.log('Socket ID', this.props.id, 'connected')
      // Add the socket ID to ES6 Set of connected sockets
      STATS_OBSERVABLE.CONNECTED_SOCKETS.add(this.props.id)
      // Set reconnecting state to false
      this.isReconnecting = false
    })

    // If debug mode enabled, also set up generalized data logger
    if (DEBUG) {
      this.socket.onSend.addListener((data) => console.log('sent', data))
    }

    this.socket.onUnpackedMessage.addListener((data) => {
      switch (data.type) {
        case GQL.DATA:
          const event = this.makeEventRow({ payload: data.payload, err: false })
          if (DEBUG) console.log('CALLED GQL.DATA CASE, GOT EVENT ROW', event)
          STATS_OBSERVABLE.DATA_EVENT_COUNT++
          this.events.push(event)
          break
        case GQL.ERROR:
          const error = this.makeEventRow({ payload: data.payload, err: true })
          if (DEBUG) console.log('CALLED GQL.ERROR CASE, GOT ERROR ROW', data)
          STATS_OBSERVABLE.ERROR_EVENT_COUNT++
          this.events.push(error)
          break
      }
    })
  }
}


/**
 * =====================
 * SOCKET MANAGER CLASS
 * =====================
 */
export interface SockerManagerConfig {
  label: string
  endpoint: string
  variables: object
  headers?: Record<string, string>
  maxConnections: number
  insertPayloadData: boolean
  connectionsPerSecond: number
  pgConnectionString: string
  subscriptionString: string
}


interface ConnectionParams {
  id: number
  socketManagerConfig: SockerManagerConfig
  queryVariables: object
}


/**
 * =======================
 * SOCKET CONNECTION CLASS
 * =======================
 */
export type FormatedError = Error & {
  originalError?: any
}


public closeSockets() {
    return Promise.all(
      Object.values(this.connections).map((conn) => {
        conn.socket.sendPacked({ type: GQL.CONNECTION_TERMINATE })
        conn.socket.close()
      })
    )
  }


public async insertEvents() {
    return Events.query()
      .allowInsert('[connection_id, event_number, event_data, event_time]')
      .insertGraph(this.allEvents)
  }


public async spawnConnection() {
    const socketId = this.nextSocketId++
    const socketManagerConfig = this.config
    const queryVariables = this.queryArgGenerator.next().value
    try {
      const connection = new Connection({
        id: socketId,
        queryVariables,
        socketManagerConfig,
      })
      connection.startSubscription()
      this.connections[socketId] = connection
      return connection
    } catch (err) {
      console.log('Caught error when calling spawnConnection(), exiting', err)
      process.exit(1)
    }
  }


private makeSocket() {
    const { endpoint, headers } = this.props.socketManagerConfig
    return new WebSocketAsPromised(endpoint, {
      createWebSocket: (url) => new WebSocket(url, 'graphql-ws', { headers }),
      extractMessageData: (event) => event,
      packMessage: (data) => JSON.stringify(data),
      unpackMessage: (data) => JSON.parse(data as string),
    } as any)
  }


// TODO: Make the retry configurable through config.yaml
@Reattempt({ times: 60, delay: 1000 })
  public async startSubscription() {
    const socket = this.socket
    const { id, queryVariables } = this.props
    const { headers, subscriptionString } = this.props.socketManagerConfig

    if (DEBUG) console.log('Socket ID', id, 'attempting to start subscription')

    await socket.open()
    socket.sendPacked({
      type: GQL.CONNECTION_INIT,
      payload: { headers },
    })
    socket.sendPacked({
      id: String(id),
      type: GQL.START,
      payload: {
        query: subscriptionString,
        variables: queryVariables,
      },
    })
  }


private makeEventRow({ payload, err }) {
    const { label, insertPayloadData } = this.props.socketManagerConfig
    return {
      label,
      is_error: err,
      operation_id: 1,
      connection_id: this.props.id,
      event_number: this.eventNumber++,
      event_data: insertPayloadData ? payload : { data: null },
      event_time: new Date().toISOString(),
    }
  }


private configureMessageHandlers() {
    // On socket close:
    this.socket.onClose.addListener(() => {
      // Print debug message if enabled
      if (DEBUG) console.log('Socket ID', this.props.id, 'closed')
      // Remove socket ID from Observable ES6 Set of connected sockets
      STATS_OBSERVABLE.CONNECTED_SOCKETS.delete(this.props.id)
      // If the socket is not currently trying to reconnect, begin trying
      if (!this.isReconnecting) this.startSubscription()
      // Set reconnecting state to true
      this.isReconnecting = true
    })

    // On socket open:
    this.socket.onOpen.addListener(() => {
      // Print debug message if enabled
      if (DEBUG) console.log('Socket ID', this.props.id, 'connected')
      // Add the socket ID to ES6 Set of connected sockets
      STATS_OBSERVABLE.CONNECTED_SOCKETS.add(this.props.id)
      // Set reconnecting state to false
      this.isReconnecting = false
    })

    // If debug mode enabled, also set up generalized data logger
    if (DEBUG) {
      this.socket.onSend.addListener((data) => console.log('sent', data))
    }

    this.socket.onUnpackedMessage.addListener((data) => {
      switch (data.type) {
        case GQL.DATA:
          const event = this.makeEventRow({ payload: data.payload, err: false })
          if (DEBUG) console.log('CALLED GQL.DATA CASE, GOT EVENT ROW', event)
          STATS_OBSERVABLE.DATA_EVENT_COUNT++
          this.events.push(event)
          break
        case GQL.ERROR:
          const error = this.makeEventRow({ payload: data.payload, err: true })
          if (DEBUG) console.log('CALLED GQL.ERROR CASE, GOT ERROR ROW', data)
          STATS_OBSERVABLE.ERROR_EVENT_COUNT++
          this.events.push(error)
          break
      }
    })
  }


@NgModule({
  imports: [
    RouterModule.forChild(routes)
  ],
  exports: [RouterModule]
})
export class UserRoutingModule { }


() => {
    const arr = new Uint8Array(32);
    const output = checksums.crc32(arr);
    const expected = 0x190A55AD
    expect(output).toEqual(expected);
}


() => {
    let output = 0
    for (let i = 0; i < 32; i++) {
        output = checksums.crc32(new Uint8Array(1), output)
    }
    const expected = 0x190A55AD;
    expect(output).toEqual(expected);
}


() => {
    const arr = Uint8Array.from(Array(32).keys());
    const output = checksums.crc32(arr);
    const expected = 0x91267E8A
    expect(output).toEqual(expected);
}


() => {
    let output = 0
    for (let i = 0; i < 32; i++) {
        output = checksums.crc32(Uint8Array.from([i]), output);
    }
    const expected = 0x91267E8A;
    expect(output).toEqual(expected);
}


() => {
    const arr = new Uint8Array(25 * 2**20);
    const output = checksums.crc32(arr);
    const expected = 0x72103906
    expect(output).toEqual(expected);
}


() => {
    const arr = new Uint8Array(32);
    const output = checksums.crc32c(arr);
    const expected = 0x8A9136AA
    expect(output).toEqual(expected);
}


() => {
    let output = 0
    for (let i = 0; i < 32; i++) {
        output = checksums.crc32c(new Uint8Array(1), output)
    }
    const expected = 0x8A9136AA;
    expect(output).toEqual(expected);
}


() => {
    const arr = Uint8Array.from(Array(32).keys());
    const output = checksums.crc32c(arr);
    const expected = 0x46DD794E
    expect(output).toEqual(expected);
}


() => {
    let output = 0
    for (let i = 0; i < 32; i++) {
        output = checksums.crc32c(Uint8Array.from([i]), output);
    }
    const expected = 0x46DD794E;
    expect(output).toEqual(expected);
}


() => {
    const arr = new Uint8Array(25 * 2**20);
    const output = checksums.crc32c(arr);
    const expected = 0xfb5b991d
    expect(output).toEqual(expected);
}


([button_code, game_button]): KeyMap => ({
                name: game_button,
                game_button: Button[game_button] ?? ControllerButton[game_button],
                button_code: Phaser.Gamepad[button_code],
            })


([game_button, button_code]): KeyMap => {
                const matches = button_code.match(/^(?:(?:(\w+) *\+ *)?(\w+) *\+ *)?(\w+)$/);
                if (!matches) console.error("Input not recognized " + button_code);
                const get_button_code = (code: string): number =>
                    gamepad_mapping.find(km => km.name === code)?.button_code ?? Phaser.Gamepad[code];
                const get_game_button = (code: string): number =>
                    gamepad_mapping.find(km => km.name === code || km.button_code === Phaser.Gamepad[code])
                        ?.game_button;
                const km: KeyMap = {name: game_button, as: button_code, game_button: EngineButton[game_button]};
                if (matches[matches.length - 2]) {
                    const buttons = matches.filter((m, i) => i && m);
                    km.button_codes = buttons.map(get_button_code).filter(bc => bc !== undefined);
                    km.game_buttons = buttons.map(get_game_button).filter(bc => bc !== undefined);
                    if (km.button_codes.length !== km.game_buttons.length)
                        console.warn(`${button_code} not well recognized!`);
                } else km.button_code = get_button_code(matches[matches.length - 1]);
                return km;
            }


(code: string): number =>
                    gamepad_mapping.find(km => km.name === code)?.button_code ?? Phaser.Gamepad[code]


km => km.name === code


(code: string): number =>
                    gamepad_mapping.find(km => km.name === code || km.button_code === Phaser.Gamepad[code])
                        ?.game_button


km => km.name === code || km.button_code === Phaser.Gamepad[code]


(m, i) => i && m


bc => bc !== undefined


([button_code, game_buttons]): KeyMap => ({
                game_button: null,
                game_buttons: game_buttons.map(gb => ControllerButton[gb]),
                button_code: Phaser.Gamepad[button_code],
            })


gb => ControllerButton[gb]


([game_button, key_code]): KeyMap => {
                const matches = key_code.match(
                    /^(?:(?:(?:(ALT|CTRL|SHIFT) *\+ *)?(ALT|CTRL|SHIFT) *\+ *)?(ALT|CTRL|SHIFT) *\+ *)?(\w+)$/
                );
                if (!matches) console.error("Input not recognized " + key_code);
                const km: KeyMap = {
                    name: game_button,
                    as: key_code,
                    game_button: Button[game_button],
                    key_code: Phaser.Keyboard[matches[matches.length - 1]],
                };
                if (matches[matches.length - 2]) {
                    const modifiers = matches.filter((m, i) => i && m);
                    km.key_modifiers = {};
                    if (modifiers.includes("ALT")) km.key_modifiers.alt = true;
                    if (modifiers.includes("CTRL")) km.key_modifiers.ctrl = true;
                    if (modifiers.includes("SHIFT")) km.key_modifiers.shift = true;
                }
                return km;
            }


km => km.button_code === button_code


km => km.game_button


km => km.key_code === key_code


button_name => {
            this.buttons[Button[button_name]] = new GamepadButton(this, Button[button_name]);
        }


button_name => {
            this.buttons[ControllerButton[button_name]] = new GamepadButton(this, ControllerButton[button_name]);
        }


bn => bn


button_name => {
            this.buttons[EngineButton[button_name]] = new GamepadButton(this, EngineButton[button_name]);
        }


(button: ControllerButton, to: AdvanceButton) => {
            this.buttons[button].on_down.add(() => this.on_gamepad_down(to));
            this.buttons[button].on_up.add(() => this.on_gamepad_up(to));
        }


() => this.on_gamepad_down(to)


() => this.on_gamepad_up(to)


km =>
                km.game_button &&
                // Check if it's the LAST button of any combo
                km.game_buttons?.[km.game_buttons.length - 1] === game_button &&
                // Check if it's any button of any combo
                // km.game_buttons?.includes(game_button) &&
                // Then if the others buttons are held down
                km.game_buttons.every(gb => gb === game_button || this.is_down(gb))


gb => gb === game_button || this.is_down(gb)


km => this._on_down(km.game_button)


km => km.game_button && km.game_buttons?.includes(game_button)


km => this._on_up(km.game_button)


(button_code: number) => {
            this.is_last_input_gamepad = true;

            const game_buttons = Gamepad.transcode_gamepad_button(button_code);
            game_buttons.forEach(game_button => this.on_gamepad_down(game_button));
        }


game_button => this.on_gamepad_down(game_button)


(button_code: number) => {
            this.is_last_input_gamepad = true;

            const game_buttons = Gamepad.transcode_gamepad_button(button_code);
            game_buttons.forEach(game_button => this.on_gamepad_up(game_button));
        }


game_button => this.on_gamepad_up(game_button)


(pad: Phaser.SinglePad, index: number, value: number) => {
            this.is_last_input_gamepad = true;

            const game_buttons = Gamepad.gamepad_stick_mapping.filter(km => km.button_code === index);
            game_buttons.forEach(km => {
                if (value < -this.stick_dead_zone) this.on_gamepad_down(km.game_buttons[0]);
                else if (value > this.stick_dead_zone) this.on_gamepad_down(km.game_buttons[1]);
                else {
                    this.on_gamepad_up(km.game_buttons[0]);
                    this.on_gamepad_up(km.game_buttons[1]);
                }
            });
        }


km => km.button_code === index


km => {
                if (value < -this.stick_dead_zone) this.on_gamepad_down(km.game_buttons[0]);
                else if (value > this.stick_dead_zone) this.on_gamepad_down(km.game_buttons[1]);
                else {
                    this.on_gamepad_up(km.game_buttons[0]);
                    this.on_gamepad_up(km.game_buttons[1]);
                }
            }


(button_code: number, value: number) => {
            this.is_last_input_gamepad = true;

            const game_buttons = Gamepad.transcode_gamepad_button(button_code);
            game_buttons.forEach(game_button =>
                value > this.trigger_dead_zone ? this.on_gamepad_down(game_button) : this.on_gamepad_up(game_button)
            );
        }


game_button =>
                value > this.trigger_dead_zone ? this.on_gamepad_down(game_button) : this.on_gamepad_up(game_button)


(event: KeyboardEvent) => {
            this.is_last_input_gamepad = false;

            const game_buttons = Gamepad.keyboard_mapping
                .filter(
                    km =>
                        km.key_code === event.keyCode &&
                        (km.key_modifiers?.alt != undefined ? km.key_modifiers.alt === event.altKey : true) &&
                        (km.key_modifiers?.ctrl != undefined ? km.key_modifiers.ctrl === event.ctrlKey : true) &&
                        (km.key_modifiers?.shift != undefined ? km.key_modifiers.shift === event.shiftKey : true)
                )
                .map(km => km.game_button);
            game_buttons.forEach(game_button => this._on_down(game_button, event));
        }


km =>
                        km.key_code === event.keyCode &&
                        (km.key_modifiers?.alt != undefined ? km.key_modifiers.alt === event.altKey : true) &&
                        (km.key_modifiers?.ctrl != undefined ? km.key_modifiers.ctrl === event.ctrlKey : true) &&
                        (km.key_modifiers?.shift != undefined ? km.key_modifiers.shift === event.shiftKey : true)


game_button => this._on_down(game_button, event)


(event: KeyboardEvent) => {
            this.is_last_input_gamepad = true;

            const game_buttons = Gamepad.transcode_keyboard_key(event.keyCode);
            game_buttons.forEach(game_button => this._on_up(game_button, event));
        }


game_button => this._on_up(game_button, event)


export class GamepadButton {
    /** Emulated gamepad controller */
    gamepad: Gamepad;
    /** Game button that will trigger */
    button: AnyButton;
    /** Debug purpose */
    name: string = undefined;
    /** Whether the gamepad button is held down */
    is_down: boolean = false;
    /** Signal to trigger when that button is pressed */
    on_down = new Phaser.Signal();
    /** Signal to trigger when that button is release */
    on_up = new Phaser.Signal();

    constructor(gamepad: Gamepad, button: AnyButton) {
        this.gamepad = gamepad;
        this.button = button;
        this.name = Button[this.button] ?? ControllerButton[this.button];
    }
    /** Whether the gamepad button is up */
    get is_up() {
        return !this.is_down;
    }
    set is_up(v: boolean) {
        this.is_down = !v;
    }
}


/** GBA buttons */
export enum AdvanceButton {
    A,
    B,
    L,
    R,
    START,
    SELECT,
    LEFT,
    RIGHT,
    UP,
    DOWN,
}


/** Other hardware controller buttons */
export enum ControllerButton {
    LB = AdvanceButton.L,
    RB = AdvanceButton.R,
    X = AdvanceButton.DOWN + 1,
    Y,
    LS,
    RS,
    L2,
    R2,
    LT = ControllerButton.L2,
    RT = ControllerButton.R2,
    // LX, LY, RX, RY,
    LLEFT,
    LRIGHT,
    LUP,
    LDOWN,
    RLEFT,
    RRIGHT,
    RUP,
    RDOWN,
}


/** Custom engine buttons */
export enum EngineButton {
    STICK_DASHING = ControllerButton.RDOWN + 1,
    PSY1,
    PSY2,
    PSY3,
    PSY4,
    ZOOM1,
    ZOOM2,
    ZOOM3,
    MUTE,
    VOL_UP,
    VOL_DOWN,
    DEBUG_PHYSICS,
    DEBUG_GRID,
    DEBUG_KEYS,
    DEBUG_STATS,
    DEBUG_FPS,
    DEBUG_SLIDERS,
    DEBUG_CAM_MINUS,
    DEBUG_CAM_PLUS,
}


/** Any buttons recognized by the engine */
export type Button = AdvanceButton | EngineButton;


/** A real controller or an engine button */
export type AnyButton = Button | ControllerButton;


/** Used to map a real button/key to an emulated controller */
type KeyMap = {
    /** Game button */
    game_button: AnyButton;
    /** Game button group that triggers the game button */
    game_buttons?: AnyButton[];
    /** For multi-button name chaining (reflect) */
    name?: string;
    /** For multi-buttons, raw button combinaison */
    as?: string;
    /** Button code of the controller button that triggers the game button */
    button_code?: number;
    /** Button codes of the controller buttons that trigger the game button, should match {@link KeyMap#game_buttons} */
    button_codes?: number[];
    /** Keycode of the keyboard key that triggers the game button */
    key_code?: number;
    /** Modifiers needed along the {@link KeyMap#key_code} */
    key_modifiers?: {alt?: boolean; ctrl?: boolean; shift?: boolean};
};