import React, { useState, useCallback, useEffect, useRef } from "react";
import { BrowserRouter as Router, Route, Routes, useParams, useNavigate } from "react-router-dom";

import Paper from "@mui/material/Paper";
import Button from "@mui/material/Button";

import { GlobalContext } from "./GlobalContext";
import { PlayerList } from "./PlayerList";
import { Chat } from "./Chat";
import { Board } from "./Board";
import { Actions } from "./Actions";
import { base, gamesPath } from "./Common";
import { GameOrder } from "./GameOrder";
import { Activities } from "./Activities";
import { SelectPlayer } from "./SelectPlayer";
import { PlayersStatus } from "./PlayersStatus";
import { ViewCard } from "./ViewCard";
import { ChooseCard } from "./ChooseCard";
import { Hand } from "./Hand";
import { Trade } from "./Trade";
import { Winner } from "./Winner";
import { HouseRules } from "./HouseRules";
import { Dice } from "./Dice";
import { assetsPath } from "./Common";

// history replaced by react-router's useNavigate
import "./App.css";
import equal from "fast-deep-equal";

type AudioEffect = HTMLAudioElement & { hasPlayed?: boolean };
const audioEffects: Record<string, AudioEffect | undefined> = {};

const loadAudio = (src: string) => {
  const audio = document.createElement("audio") as AudioEffect;
  audio.src = `${assetsPath}/${src}`;
  audio.setAttribute("preload", "auto");
  audio.setAttribute("controls", "none");
  audio.style.display = "none";
  document.body.appendChild(audio);
  void audio.play();
  audio.hasPlayed = true;
  return audio;
};

const Table: React.FC = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [gameId, setGameId] = useState<string | undefined>(params.gameId ? (params.gameId as string) : undefined);
  const [ws, setWs] = useState<WebSocket | undefined>(undefined); /* tracks full websocket lifetime */
  const [connection, setConnection] = useState<WebSocket | undefined>(undefined); /* set after ws is in OPEN */
  const [retryConnection, setRetryConnection] =
    useState<boolean>(true); /* set when connection should be re-established */
  const [name, setName] = useState<string>("");
  const [error, setError] = useState<string | undefined>(undefined);
  const [warning, setWarning] = useState<string | undefined>(undefined);
  const [loaded, setLoaded] = useState<boolean>(false);

  type Turn = { color?: string; roll?: number; actions?: string[]; select?: Record<string, number> };
  type PrivateType = { name?: string; color?: string; turnNotice?: string };

  const [dice, setDice] = useState<number[] | undefined>(undefined);
  const [state, setState] = useState<string | undefined>(undefined);
  const [color, setColor] = useState<string | undefined>(undefined);
  const [priv, setPriv] = useState<PrivateType | undefined>(undefined);
  const [turn, setTurn] = useState<Turn | undefined>(undefined);
  const [buildActive, setBuildActive] = useState<boolean>(false);
  const [tradeActive, setTradeActive] = useState<boolean>(false);
  const [cardActive, setCardActive] = useState<unknown>(undefined);
  const [houseRulesActive, setHouseRulesActive] = useState<boolean>(false);
  const [winnerDismissed, setWinnerDismissed] = useState<boolean>(false);
  const [global, setGlobal] = useState<Record<string, unknown>>({});
  const [count, setCount] = useState<number>(0);
  const [audio, setAudio] = useState<boolean>(
    localStorage.getItem("audio") ? JSON.parse(localStorage.getItem("audio") as string) : false
  );
  const [animations, setAnimations] = useState<boolean>(
    localStorage.getItem("animations") ? JSON.parse(localStorage.getItem("animations") as string) : false
  );
  const [volume, setVolume] = useState<number>(
    localStorage.getItem("volume") ? parseFloat(localStorage.getItem("volume") as string) : 0.5
  );
  const fields = ["id", "state", "color", "name", "private", "dice", "turn"];

  const onWsOpen = (event: Event) => {
    console.log(`ws: open`);
    setError("");

    setConnection(ws);
    const sock = event.target as WebSocket;
    sock.send(JSON.stringify({ type: "game-update" }));
    sock.send(JSON.stringify({ type: "get", fields }));
  };

  const onWsMessage = (event: MessageEvent) => {
    const data = JSON.parse(event.data as string);
    switch (data.type) {
      case "error":
        console.error(`App - error`, data.error);
        setError(data.error);
        break;
      case "warning":
        console.warn(`App - warning`, data.warning);
        setWarning(data.warning);
        setTimeout(() => {
          setWarning("");
        }, 3000);
        break;
      case "game-update":
        if (!loaded) {
          setLoaded(true);
        }
        console.log(`app - message - ${data.type}`, data.update);

        if ("private" in data.update && !equal(priv, data.update.private)) {
          const priv = data.update.private;
          if (priv.name !== name) {
            setName(priv.name);
          }
          if (priv.color !== color) {
            setColor(priv.color);
          }
          setPriv(priv);
        }

        if ("name" in data.update) {
          if (data.update.name) {
            setName(data.update.name);
          } else {
            setWarning("");
            setError("");
            setPriv(undefined);
          }
        }
        if ("id" in data.update && data.update.id !== gameId) {
          setGameId(data.update.id);
        }
        if ("state" in data.update && data.update.state !== state) {
          if (data.update.state !== "winner" && winnerDismissed) {
            setWinnerDismissed(false);
          }
          setState(data.update.state);
        }
        if ("dice" in data.update && !equal(data.update.dice, dice)) {
          setDice(data.update.dice);
        }
        if ("turn" in data.update && !equal(data.update.turn, turn)) {
          setTurn(data.update.turn);
        }
        if ("color" in data.update && data.update.color !== color) {
          setColor(data.update.color);
        }
        break;
      default:
        break;
    }
  };

  const sendUpdate = (update: unknown) => {
    if (ws) ws.send(JSON.stringify(update));
  };

  const cbResetConnection = useCallback(() => {
    let timer: number | null = null;
    function reset() {
      timer = null;
      setRetryConnection(true);
    }
    return () => {
      if (timer) {
        clearTimeout(timer);
      }
      timer = window.setTimeout(reset, 5000);
    };
  }, [setRetryConnection]);

  const resetConnection = cbResetConnection();

  if (global.ws !== connection || global.name !== name || global.gameId !== gameId) {
    setGlobal({
      ws: connection,
      name,
      gameId,
    });
  }

  const onWsError = () => {
    const error =
      `Connection to Ketr Ketran game server failed! ` + `Connection attempt will be retried every 5 seconds.`;
    setError(error);
    setGlobal(Object.assign({}, global, { ws: undefined }));
    setWs(undefined); /* clear the socket */
    setConnection(undefined); /* clear the connection */
    resetConnection();
  };

  const onWsClose = () => {
    const error = `Connection to Ketr Ketran game was lost. ` + `Attempting to reconnect...`;
    setError(error);
    setGlobal(Object.assign({}, global, { ws: undefined }));
    setWs(undefined); /* clear the socket */
    setConnection(undefined); /* clear the connection */
    resetConnection();
  };

  const refWsOpen = useRef<(e: Event) => void>(() => {});
  useEffect(() => {
    refWsOpen.current = onWsOpen;
  }, [onWsOpen]);
  const refWsMessage = useRef<(e: MessageEvent) => void>(() => {});
  useEffect(() => {
    refWsMessage.current = onWsMessage;
  }, [onWsMessage]);
  const refWsClose = useRef<(e: CloseEvent) => void>(() => {});
  useEffect(() => {
    refWsClose.current = onWsClose;
  }, [onWsClose]);
  const refWsError = useRef<(e: Event) => void>(() => {});
  useEffect(() => {
    refWsError.current = onWsError;
  }, [onWsError]);

  useEffect(() => {
    if (gameId) {
      return;
    }

    window
      .fetch(`${base}/api/v1/games/`, {
        method: "POST",
        cache: "no-cache",
        credentials: "same-origin",
        headers: {
          "Content-Type": "application/json",
        },
      })
      .then((res) => {
        if (res.status >= 400) {
          const error =
            `Unable to connect to Ketr Ketran game server! ` + `Try refreshing your browser in a few seconds.`;
          setError(error);
          throw new Error(error);
        }
        return res.json();
      })
      .then((update) => {
        if (update.id !== gameId) {
          navigate(`/${update.id}`);
          setGameId(update.id);
        }
      })
      .catch((error) => {
        console.error(error);
      });
  }, [gameId, setGameId]);

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

    const unbind = () => {
      console.log(`table - unbind`);
    };

    if (!ws && !connection && retryConnection) {
      const loc = window.location;
      let new_uri = "";
      if (loc.protocol === "https:") {
        new_uri = "wss";
      } else {
        new_uri = "ws";
      }
      new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${gameId}?${count}`;
      setWs(new WebSocket(new_uri));
      setConnection(undefined);
      setRetryConnection(false);
      setCount(count + 1);
      return unbind;
    }

    if (!ws) {
      return unbind;
    }

    const cbOpen = (e: Event) => refWsOpen.current(e);
    const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
    const cbClose = (e: CloseEvent) => refWsClose.current(e);
    const cbError = (e: Event) => refWsError.current(e);

    ws.addEventListener("open", cbOpen);
    ws.addEventListener("close", cbClose);
    ws.addEventListener("error", cbError);
    ws.addEventListener("message", cbMessage);

    return () => {
      unbind();
      ws.removeEventListener("open", cbOpen);
      ws.removeEventListener("close", cbClose);
      ws.removeEventListener("error", cbError);
      ws.removeEventListener("message", cbMessage);
    };
  }, [
    ws,
    setWs,
    connection,
    setConnection,
    retryConnection,
    setRetryConnection,
    gameId,
    refWsOpen,
    refWsMessage,
    refWsClose,
    refWsError,
    count,
    setCount,
  ]);

  useEffect(() => {
    if (state === "volcano") {
      if (!audioEffects.volcano) {
        audioEffects.volcano = loadAudio("volcano-eruption.mp3");
        audioEffects.volcano.volume = volume * volume;
      } else {
        if (!audioEffects.volcano.hasPlayed) {
          audioEffects.volcano.hasPlayed = true;
          audioEffects.volcano.play();
        }
      }
    } else {
      if (audioEffects.volcano) {
        audioEffects.volcano.hasPlayed = false;
      }
    }
  }, [state, volume]);

  useEffect(() => {
    if (turn && turn.color === color && state !== "lobby") {
      if (!audioEffects.yourTurn) {
        audioEffects.yourTurn = loadAudio("its-your-turn.mp3");
        audioEffects.yourTurn.volume = volume * volume;
      } else {
        if (!audioEffects.yourTurn.hasPlayed) {
          audioEffects.yourTurn.hasPlayed = true;
          audioEffects.yourTurn.play();
        }
      }
    } else if (turn) {
      if (audioEffects.yourTurn) {
        audioEffects.yourTurn.hasPlayed = false;
      }
    }

    if (turn && turn.roll === 7) {
      if (!audioEffects.robber) {
        audioEffects.robber = loadAudio("robber.mp3");
        audioEffects.robber.volume = volume * volume;
      } else {
        if (!audioEffects.robber.hasPlayed) {
          audioEffects.robber.hasPlayed = true;
          audioEffects.robber.play();
        }
      }
    } else if (turn) {
      if (audioEffects.robber) {
        audioEffects.robber.hasPlayed = false;
      }
    }

    if (turn && turn.actions && turn.actions.indexOf("playing-knight") !== -1) {
      if (!audioEffects.knights) {
        audioEffects.knights = loadAudio("the-knights-who-say-ni.mp3");
        audioEffects.knights.volume = volume * volume;
      } else {
        if (!audioEffects.knights.hasPlayed) {
          audioEffects.knights.hasPlayed = true;
          audioEffects.knights.play();
        }
      }
    } else if (turn && turn.actions && turn.actions.indexOf("playing-knight") === -1) {
      if (audioEffects.knights) {
        audioEffects.knights.hasPlayed = false;
      }
    }
  }, [state, turn, color, volume]);

  useEffect(() => {
    for (const key in audioEffects) {
      audioEffects[key].volume = volume * volume;
    }
  }, [volume]);

  return (
    <GlobalContext.Provider value={global}>
      {/* <PingPong/> */}
      <div className="Table">
        <div className="ActivitiesBox">
          <Activities />
          {dice && dice.length && (
            <div className="DiceRoll">
              {dice.length === 1 && <div>Volcano roll!</div>}
              {dice.length === 2 && <div>Current roll</div>}
              <div>
                <Dice pips={dice[0]} />
                {dice.length === 2 && <Dice pips={dice[1]} />}
              </div>
            </div>
          )}
        </div>
        <div className="Game">
          <div className="Dialogs">
            {error && (
              <div className="Dialog ErrorDialog">
                <Paper className="Error">
                  <div>{error}</div>
                  <Button
                    onClick={() => {
                      setError("");
                    }}
                  >
                    dismiss
                  </Button>
                </Paper>
              </div>
            )}

            {priv && priv.turnNotice && (
              <div className="Dialog TurnNoticeDialog">
                <Paper className="TurnNotice">
                  <div>{priv.turnNotice}</div>
                  <Button
                    onClick={() => {
                      sendUpdate({ type: "turn-notice" });
                    }}
                  >
                    dismiss
                  </Button>
                </Paper>
              </div>
            )}
            {warning && (
              <div className="Dialog WarningDialog">
                <Paper className="Warning">
                  <div>{warning}</div>
                  <Button
                    onClick={() => {
                      setWarning("");
                    }}
                  >
                    dismiss
                  </Button>
                </Paper>
              </div>
            )}
            {state === "normal" && <SelectPlayer />}
            {color && state === "game-order" && <GameOrder />}

            {!winnerDismissed && <Winner {...{ winnerDismissed, setWinnerDismissed }} />}
            {houseRulesActive && <HouseRules {...{ houseRulesActive, setHouseRulesActive }} />}
            <ViewCard {...{ cardActive, setCardActive }} />
            <ChooseCard />
          </div>

          <Board animations={animations} />
          <PlayersStatus active={false} />
          <PlayersStatus active={true} />
          <Hand {...{ buildActive, setBuildActive, setCardActive }} />
        </div>
        <div className="Sidebar">
          {name !== "" && volume !== undefined && (
            <Paper className="Volume">
              <div>Audio effects</div>{" "}
              <input
                type="checkbox"
                id="audio"
                name="audio"
                defaultChecked={audio ? true : false}
                onInput={() => {
                  const value = !audio;
                  localStorage.setItem("audio", JSON.stringify(value));
                  setAudio(value);
                }}
              />
              <div>Sound effects volume</div>{" "}
              <input
                type="range"
                id="volume"
                name="volume"
                value={volume * 100}
                min="0"
                max="100"
                onInput={(e) => {
                  const alpha = parseFloat((e.currentTarget as HTMLInputElement).value) / 100;

                  localStorage.setItem("volume", alpha.toString());
                  setVolume(alpha);
                }}
              />
              <div>Animations</div>{" "}
              <input
                type="checkbox"
                id="animations"
                name="animations"
                defaultChecked={animations ? true : false}
                onInput={() => {
                  const value = !animations;
                  localStorage.setItem("animations", JSON.stringify(value));
                  setAnimations(value);
                }}
              />
            </Paper>
          )}
          {name !== "" && <PlayerList />}
          {/* Trade is an untyped JS component; assert its type to avoid `any` */}
          {(() => {
            const TradeComponent = Trade as unknown as React.ComponentType<{
              tradeActive: boolean;
              setTradeActive: (v: boolean) => void;
            }>;
            return <TradeComponent tradeActive={tradeActive} setTradeActive={setTradeActive} />;
          })()}
          {name !== "" && <Chat />}
          {/* name !== "" && <VideoFeeds/> */}
          {loaded && (
            <Actions
              {...{ buildActive, setBuildActive, tradeActive, setTradeActive, houseRulesActive, setHouseRulesActive }}
            />
          )}
        </div>
      </div>
    </GlobalContext.Provider>
  );
};

const App: React.FC = () => {
  const [playerId, setPlayerId] = useState<string | undefined>(undefined);
  const [error, setError] = useState<string | undefined>(undefined);

  useEffect(() => {
    if (playerId) {
      return;
    }
    window
      .fetch(`${base}/api/v1/games/`, {
        method: "GET",
        cache: "no-cache",
        credentials: "same-origin" /* include cookies */,
        headers: {
          "Content-Type": "application/json",
        },
      })
      .then((res) => {
        if (res.status >= 400) {
          const error =
            `Unable to connect to Ketr Ketran game server! ` + `Try refreshing your browser in a few seconds.`;
          setError(error);
        }
        return res.json();
      })
      .then((data) => {
        setPlayerId(data.player);
      })
      .catch(() => {});
  }, [playerId, setPlayerId]);

  if (!playerId) {
    return <>{error}</>;
  }

  return (
    <Router basename={base}>
      <Routes>
        <Route element={<Table />} path="/:gameId" />
        <Route element={<Table />} path="/" />
      </Routes>
    </Router>
  );
};

export default App;
