import {useDispatch, useSelector} from "react-redux";
import {AppState} from "../../../store/store";
import React, {useCallback, useEffect, useRef, useState} from "react";
import useComponentResizeListener from "../../../hooks/componentResizeListener";
import styles from "./Cup.module.css";
import cupDrawer from "./Cup.drawer";
import cupTools from "./Cup.tools";
import cupOptions from "./Cup.options";
import {CupItem} from "../../../hooks/rustWsServer";
import {
  Box, Checkbox,
  Dialog,
  DialogContent,
  DialogTitle, FormControlLabel,
  IconButton,
  TextField, Typography,
  useTheme,
} from "@mui/material";
import {AddRounded, RemoveRounded, SettingsRounded} from "@mui/icons-material";
import {setCupParams} from "../../../store/cupSlice";
import {CupProps} from "./Cup.props";

export interface CanvasSize {
  width: number;
  height: number;
}

const Cup = ({workerRef}: CupProps) => {
  const symbol = useSelector((state: AppState) => state.screenerSlice.symbol);
  const cupParams = useSelector((state: AppState) => state.cupSlice);
  const [dpiScale, setDpiScale] = useState(Math.ceil(window.devicePixelRatio));
  const [canvasSize, setCanvasSize] = useState<CanvasSize>({height: 0, width: 0});
  const containerRef = useRef<HTMLDivElement|null>(null);
  const canvasRef = useRef<HTMLCanvasElement|null>(null);
  const [zoom, setZoom] = useState(1);
  const [isLoaded, setIsLoaded] = useState(false);
  const [settings, setSettings] = useState({
    timeout: "40",
    maxVolume: window.localStorage.getItem(`cup-volume-${symbol}`) || "1000",
    isFixedVolume: null !== window.localStorage.getItem(`cup-volume-${symbol}`),
  });
  const [showSettings, setShowSettings] = useState(false);
  const size = useComponentResizeListener(canvasRef);
  const darkMode = useTheme().palette.mode === "dark";
  const dispatch = useDispatch();

  const cupSubscribe = useCallback(async(pair: string, zoom: number) => {
    workerRef.current?.postMessage(JSON.stringify({type: "subscribe", pair, zoom}));
  }, []);

  const cupUnsubscribe = useCallback(async(pair: string) => {
    workerRef.current?.postMessage(JSON.stringify({type: "unsubscribe", pair}));
  }, []);

  const sendSettings = () => {
    workerRef.current?.postMessage(JSON.stringify({
      type: "change_max_volume",
      isFixed: settings.isFixedVolume,
      maxVolume: parseInt(settings.maxVolume || "0") < 0 ? 1 : parseInt(settings.maxVolume),
    }));
    workerRef.current?.postMessage(JSON.stringify({
      type: "change_publisher_timeout",
      value: parseInt(settings.timeout || "0") < 40 ? 40 : parseInt(settings.timeout),
    }));
  };

  const wheelHandler = (e: WheelEvent) => {
    e.preventDefault();
    workerRef.current?.postMessage(JSON.stringify({type: e.deltaY < 0 ? "camera_up" : "camera_down"}));
  };

  const zoomAdd = () => {
    let newZoom;

    if (zoom >= 1 && zoom < 10) {
      newZoom = zoom + 1;
    } else if (zoom >= 10 && zoom < 30) {
      newZoom = zoom + 5;
    } else if (zoom >= 30 && zoom < 100) {
      newZoom = zoom + 10;
    } else {
      newZoom = zoom;
    }

    setZoom(newZoom);
  };

  const zoomSub = () => {
    let newZoom;

    if (zoom > 1 && zoom <= 10) {
      newZoom = zoom - 1;
    } else if (zoom > 10 && zoom <= 30) {
      newZoom = zoom - 5;
    } else if (zoom > 30 && zoom <= 100) {
      newZoom = zoom - 10;
    } else {
      newZoom = zoom;
    }

    setZoom(newZoom);
  };

  useEffect(() => {
    workerRef.current = new Worker(new URL("/workers/cup-builder.ts", import.meta.url));
    canvasRef.current?.addEventListener("wheel", wheelHandler, {passive: false});
    sendSettings();

    return () => {
      workerRef.current?.terminate();
      canvasRef.current?.removeEventListener("wheel", wheelHandler);
    };
  }, []);

  useEffect(() => {
    if (!workerRef.current) return;
    let animationFrameId: number|null = null;

    workerRef.current.onmessage = (event: MessageEvent<{
      type: string,
      camera: number,
      aggregation: number,
      bestBidPrice: number,
      bestAskPrice: number,
      maxVolume: number,
      pricePrecision: number,
      quantityPrecision: number,
      priceStep: number,
      cup: {[key: number]: CupItem},
      rowsCount: number,
    }>) => {
      if (event?.data?.type === "update_cup") {
        if (null !== animationFrameId) {
          cancelAnimationFrame(animationFrameId);
        }

        animationFrameId = requestAnimationFrame(() => {
          const context = canvasRef.current?.getContext("2d");
          const zoomedTickSize = event.data.priceStep * event.data.aggregation;

          if (context) {
            const rowsOnScreenCount = cupTools.getRowsCountOnScreen(
              canvasSize.height,
              cupOptions().cell.defaultHeight * dpiScale,
            );
            const realCellHeight = parseInt((canvasSize.height / rowsOnScreenCount).toFixed(0));

            if (event.data.rowsCount !== rowsOnScreenCount) {
              workerRef.current?.postMessage(JSON.stringify({type: "change_rows_count", value: rowsOnScreenCount}));
            }

            cupDrawer.clear(context, canvasSize);

            if (cupParams.rowCount !== rowsOnScreenCount
              || cupParams.cellHeight !== realCellHeight
              || cupParams.aggregation !== event.data.aggregation
            ) {
              dispatch(setCupParams({
                aggregation: event.data.aggregation,
                rowCount: rowsOnScreenCount,
                cellHeight: realCellHeight,
                pricePrecision: event.data.pricePrecision,
                priceStep: event.data.priceStep,
                quantityPrecision: event.data.quantityPrecision,
              }));
            }

            if (event.data.camera === 0 && isLoaded) {
              setIsLoaded(false);
            }

            if (event.data.camera > 0 && !isLoaded) {
              setIsLoaded(true);
            }

            if (event.data.camera !== 0) {
              cupDrawer.draw(
                context,
                canvasSize,
                dpiScale,
                event.data.bestBidPrice,
                event.data.bestAskPrice,
                event.data.maxVolume,
                event.data.pricePrecision,
                event.data.quantityPrecision,
                event.data.priceStep,
                event.data.aggregation,
                rowsOnScreenCount,
                event.data.camera,
                realCellHeight,
                {
                  buy: parseInt((Math.floor(event.data.bestBidPrice / zoomedTickSize) * zoomedTickSize).toFixed(0)),
                  sell: parseInt((Math.ceil(event.data.bestAskPrice / zoomedTickSize) * zoomedTickSize).toFixed(0)),
                },
                darkMode,
                event.data.cup,
              );
            }
          }
        });
      }
    };

    return () => {
      if (null !== animationFrameId) {
        cancelAnimationFrame(animationFrameId);
      }
    };
  }, [workerRef.current, canvasSize, darkMode, dpiScale, isLoaded]);

  useEffect(() => {
    setDpiScale(Math.ceil(window.devicePixelRatio));
  }, [window.devicePixelRatio]);

  useEffect(() => {
    if (!size) {
      return;
    }

    setCanvasSize({
      width: Math.floor(size.width) * dpiScale,
      height: Math.floor(size.height) * dpiScale,
    });
  }, [dpiScale, size]);

  useEffect(() => {
    cupSubscribe(symbol.toUpperCase(), zoom);

    return () => {
      cupUnsubscribe(symbol.toUpperCase());
    };
  }, [symbol, zoom]);

  useEffect(() => {
    setSettings({
      ...settings,
      maxVolume: window.localStorage.getItem(`cup-volume-${symbol}`) || "1000",
      isFixedVolume: null !== window.localStorage.getItem(`cup-volume-${symbol}`),
    });
  }, [symbol]);

  useEffect(() => {
    if (settings.isFixedVolume) {
      window.localStorage.setItem(`cup-volume-${symbol}`, settings.maxVolume);
    } else {
      window.localStorage.removeItem(`cup-volume-${symbol}`);
    }

    sendSettings();
  }, [settings]);

  return <div ref={containerRef} className={styles.canvasWrapper}>
    <div className={styles.controls}>
      <IconButton onClick={zoomSub} size="small" disabled={!isLoaded}>
        <RemoveRounded />
      </IconButton>
      <span>x{zoom}</span>
      <IconButton onClick={zoomAdd} size="small" disabled={!isLoaded}>
        <AddRounded />
      </IconButton>
      <IconButton onClick={() => setShowSettings(true)} size="small">
        <SettingsRounded
          // @ts-ignore
          fontSize="xx-small"
        />
      </IconButton>
    </div>
    <canvas
      ref={canvasRef}
      className={[styles.canvas, isLoaded ? "" : styles.loading].join(" ")}
      width={canvasSize?.width}
      height={canvasSize?.height}
    />

    <Dialog open={showSettings} onClose={() => setShowSettings(false)} fullWidth maxWidth="xs">
      <DialogTitle>Настройки стакана</DialogTitle>
      <DialogContent dividers>
        <Box
          component="form"
          autoComplete="off"
          onSubmit={() => {}}
        >
          <TextField
            fullWidth
            label="Таймаут обновления стакана, мс."
            variant="standard"
            margin="normal"
            value={settings.timeout}
            onChange={e => {
              setSettings({...settings, timeout: e.target.value});
            }}
          />
          <Typography fontSize="xx-small">Минимальный таймаут - 40 мс.</Typography>

          <FormControlLabel
            control={<Checkbox value="isFixedVolume" checked={settings.isFixedVolume} color="primary" />}
            label="Фиксированный объем."
            onChange={() => setSettings({...settings, isFixedVolume: !settings.isFixedVolume})}
          />

          {settings.isFixedVolume && (
            <TextField
              fullWidth
              label="Объем стакана."
              variant="standard"
              margin="normal"
              value={settings.maxVolume}
              onChange={e => {
                setSettings({...settings, maxVolume: e.target.value});
              }}
            />
          )}
        </Box>
      </DialogContent>
    </Dialog>
  </div>;
};

export default Cup;




const OrderFeed = ({workerRef}: OrderFeedProps) => {
  const symbol = useSelector((state: AppState) => state.screenerSlice.symbol);
  const cupParams = useSelector((state: AppState) => state.cupSlice);
  const [dpiScale, setDpiScale] = useState(Math.ceil(window.devicePixelRatio));
  const [canvasSize, setCanvasSize] = useState<CanvasSize>({height: 0, width: 0});
  const containerRef = useRef<HTMLDivElement|null>(null);
  const canvasRef = useRef<HTMLCanvasElement|null>(null);
  const [settings, setSettings] = useState({
    timeout: "40",
    aggregation: true,
    minQuantity: window.localStorage.getItem(`order-feed-min-qty-${symbol}`) || "0",
  });
  const [showSettings, setShowSettings] = useState(false);
  const size = useComponentResizeListener(canvasRef);
  const theme = useTheme();

  const draw = (trades: TradeItem[], camera: number) => {
    if (null === canvasRef || !Array.isArray(trades)) return;

    const context = canvasRef.current?.getContext("2d");

    if (context) {
      orderFeedDrawer.clear(context, canvasSize);
      orderFeedDrawer.drawLine(
        context,
        canvasSize,
        trades,
        camera,
        cupParams.aggregatedPriceStep,
        cupParams.cellHeight,
        cupParams.quantityDivider,
        !settings.minQuantity ? 0 : parseFloat(settings.minQuantity),
        dpiScale,
        theme,
      );
      orderFeedDrawer.drawChain(
        context,
        canvasSize,
        trades,
        camera,
        cupParams.aggregatedPriceStep,
        cupParams.cellHeight,
        cupParams.quantityDivider,
        !settings.minQuantity ? 0 : parseFloat(settings.minQuantity),
        dpiScale,
        theme,
      );
    }
  };

  const wheelHandler = (event: WheelEvent) => {
    event.preventDefault();
    const scrollAmount = event.deltaY;

    if (containerRef.current) {
      containerRef.current.scrollLeft += scrollAmount;
    }
  };

  const tradeSubscribe = useCallback(async(symbol: string) => {
    workerRef.current?.postMessage(JSON.stringify({type: "subscribe", symbol}));
  }, []);

  const tradeUnsubscribe = useCallback(async(symbol: string) => {
    workerRef.current?.postMessage(JSON.stringify({type: "unsubscribe", symbol}));
  }, []);

  useEffect(() => {
    if (!workerRef.current) return;
    let animationFrameId: number|null = null;

    workerRef.current.onmessage = (event: MessageEvent<any>) => {
      if (event.data.type === "update_trades") {
        if (null !== animationFrameId) {
          cancelAnimationFrame(animationFrameId);
        }

        animationFrameId = requestAnimationFrame(() => draw(event.data.trades, event.data.camera));
      }
    };

    return () => {
      if (null !== animationFrameId) {
        cancelAnimationFrame(animationFrameId);
      }
    };
  }, [workerRef.current, canvasRef.current, canvasSize, symbol, cupParams, theme, settings.minQuantity]);

  useEffect(() => {
    setDpiScale(Math.ceil(window.devicePixelRatio));
  }, [window.devicePixelRatio]);

  useEffect(() => {
    if (!size) {
      return;
    }

    setCanvasSize({
      width: Math.floor(size.width) * dpiScale,
      height: Math.floor(size.height) * dpiScale,
    });
  }, [dpiScale, size]);

  useEffect(() => {
    tradeSubscribe(symbol);
    setSettings(state => ({
      ...state,
      minQuantity: window.localStorage.getItem(`order-feed-min-qty-${symbol}`) || "0",
    }));

    return () => {
      tradeUnsubscribe(symbol);
    };
  }, [symbol, workerRef.current]);

  useEffect(() => {
    containerRef.current?.scroll(800, 0);
    containerRef.current?.addEventListener("wheel", wheelHandler, {passive: false});
    workerRef.current = new Worker(new URL("/workers/order-feed-builder.ts", import.meta.url));

    return () => {
      containerRef.current?.removeEventListener("wheel", wheelHandler);
      workerRef.current?.terminate();
    };
  }, []);

  useEffect(() => {
    workerRef.current?.postMessage(JSON.stringify({
      type: "change_publisher_timeout",
      value: parseInt(settings.timeout || "0") < 40 ? 40 : parseInt(settings.timeout),
    }));
    workerRef.current?.postMessage(JSON.stringify({
      type: "change_aggregation",
      value: settings.aggregation,
    }));
    window.localStorage.setItem(`order-feed-min-qty-${symbol}`, settings.minQuantity);
  }, [settings]);

  return <Box ref={containerRef} sx={{
    scrollbarWidth: "thin",
    "&::-webkit-scrollbar-track": {
      background: theme => theme.palette.background.paper,
    },
    "&::-webkit-scrollbar-thumb": {
      backgroundColor: theme => theme.palette.grey[300],
      border: theme => `3px solid ${theme.palette.white.main}`,
    },
  }} className={`${styles.canvasWrapper} scroll-block`}>
    <div className={styles.controls}>
      <IconButton onClick={() => setShowSettings(true)} size="small">
        <SettingsRounded
          // @ts-ignore
          fontSize="xx-small"
        />
      </IconButton>
    </div>
    <canvas
      ref={canvasRef}
      className={styles.canvas}
      width={canvasSize?.width}
      height={canvasSize?.height}
    />

    <Dialog open={showSettings} onClose={() => setShowSettings(false)} fullWidth maxWidth="xs">
      <DialogTitle>Настройки списка ордеров</DialogTitle>
      <DialogContent dividers>
        <Box
          component="form"
          autoComplete="off"
          onSubmit={() => {}}
        >
          <TextField
            fullWidth
            label="Минимальный отображаемый объем"
            variant="standard"
            margin="normal"
            value={settings.minQuantity}
            onChange={e => {
              setSettings({...settings, minQuantity: e.target.value});
            }}
          />

          <TextField
            fullWidth
            label="Таймаут обновления стакана, мс."
            variant="standard"
            margin="normal"
            value={settings.timeout}
            onChange={e => {
              setSettings({...settings, timeout: e.target.value});
            }}
          />
          <Typography fontSize="xx-small">Минимальный таймаут - 40 мс.</Typography>

          <FormControlLabel
            control={<Checkbox value="isFixedVolume" checked={settings.aggregation} color="primary" />}
            label="Агрегация сделок."
            onChange={() => setSettings({...settings, aggregation: !settings.aggregation})}
          />

        </Box>
      </DialogContent>
    </Dialog>
  </Box>;
};

export default OrderFeed;

const TradingCup = () => {
  const cupWorkerRef = useRef<Worker>();
  const orderFeedWorkerRef = useRef<Worker>();

  useEffect(() => {
    const channel = new MessageChannel();
    cupWorkerRef.current?.postMessage({type: "set_port", port: channel.port1}, [channel.port1]);
    orderFeedWorkerRef.current?.postMessage({type: "set_port", port: channel.port2}, [channel.port2]);
  }, []);

  return <Stack direction="row" height="100%">
    <div className={styles.OfferFeed}>
      <OrderFeed workerRef={orderFeedWorkerRef} />
    </div>
    <div className={styles.Cup}>
      <Cup workerRef={cupWorkerRef} />
    </div>
  </Stack>;
};

export default TradingCup;


нужно настройки OrderFeed и Cup объединить в одно окно и кнопку. Нужно вынести этот попап в TradingCup компонент. Оттуда есть доступ к обоим воркерам.