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

import { GlobalContext, GlobalData } from "./GlobalContext";
import { PersonList } from "./PersonList";
import { Chat } from "./Chat";

import "./App.css";
import equal from "fast-deep-equal";

// @ts-ignore
const base = process.env.REACT_APP_CHAT_BASE;

const Table = () => {
  const params = useParams();
  const [chatId, setChatId] = useState(params.chatId ? params.chatId : undefined);
  const [ws, setWs] = useState <WebSocket | undefined>(); /* tracks full websocket lifetime */
  const [connection, setConnection] = useState<WebSocket|undefined>(undefined); /* set after ws is in OPEN */
  const [retryConnection, setRetryConnection] = useState(true); /* set when connection should be re-established */
  const [name, setName] = useState("");
  const [loaded, setLoaded] = useState(false);

  const [color, setColor] = useState(undefined);
  const [priv, setPriv] = useState(undefined);
  const [global, setGlobal] = useState<GlobalData>({
    chatId: undefined,
    ws: undefined,
    name: '',
    chat: []
  });
  const [count, setCount] = useState(0);
  const [error, setError] = useState<string>('');
  const [warning, setWarning] = useState<string>('');

  const fields = ['id', 'state', 'color', 'name', 'private' ];
  const navigate = useNavigate();
  const onWsOpen = (event: any) => {
    console.log(`ws: open`);
    setError("");

    /* We do not set the socket as connected until the 'open' message
     * comes through */
    setConnection(ws);

    /* Request a full chat-update
     * We only need chatId and name for App, however in the event
     * of a network disconnect, we need to refresh the entire chat
     * state on reload so all bound components reflect the latest
     * state */
    event.target.send(JSON.stringify({
      type: 'chat-update'
    }));
    event.target.send(JSON.stringify({
      type: 'get',
      fields
    }));
  };

  const onWsMessage = (event: any) => {
    const data = JSON.parse(event.data);
    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(() => {
          console.log(`todo: stack warnings in a window and have them disappear one at a time.`);
          console.log(`app - clearing warning`);
          setWarning("");
        }, 3000);
        break;
      case 'chat-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) {
            console.log(`App - setting name (via private): ${priv.name}`);
            setName(priv.name);
          }
          if (priv.color !== color) {
            console.log(`App - setting color (via private): ${priv.color}`);
            setColor(priv.color);
          }
          setPriv(priv);
        }

        if ('name' in data.update) {
          if (data.update.name) {
            console.log(`App - setting name: ${data.update.name}`);
            setName(data.update.name);
          } else {
            setWarning("");
            setError("");
            setPriv(undefined);
          }
        }
        if ('id' in data.update && data.update.id !== chatId) {
          console.log(`App - setting chatId ${data.update.id}`);
          setChatId(data.update.id);
        }
        if ('color' in data.update && data.update.color !== color) {
          console.log(`App - setting color: ${color}`);
          setColor(data.update.color);
        }
        break;
      default:
        break;
    }
  };

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

  const cbResetConnection = useCallback(() => {
    let timer: any = 0;
    function reset() {
      timer = 0;
      setRetryConnection(true);
    };
    return () => {
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(reset, 5000);
    };
  }, [setRetryConnection]);

  const resetConnection = cbResetConnection();

  if (global.ws !== connection
    || global.name !== name
    || global.chatId !== chatId) {
    console.log(`board - (app) - setting global`, global, {
      ws: connection,
      name,
      chatId
    });
    setGlobal({
      ws: connection,
      name,
      chatId,
      chat: []
    });
  }

  const onWsError = (event: any) => {
    console.error(`ws: error`, event);
    const error = `Connection to Ketr Ketran chat 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 = (event: any) => {
    const error = `Connection to Ketr Ketran chat was lost. ` +
      `Attempting to reconnect...`;
    console.warn(`ws: close`);
    setError(error);
    setGlobal(Object.assign({}, global, { ws: undefined }));
    setWs(undefined); /* clear the socket */
    setConnection(undefined); /* clear the connection */
    resetConnection();
  };

  /* callback refs are used to provide correct state reference
   * in the callback handlers, while also preventing rebinding
   * of event handlers on every render */
  const refWsOpen = useRef(onWsOpen);
  useEffect(() => { refWsOpen.current = onWsOpen; });
  const refWsMessage = useRef(onWsMessage);
  useEffect(() => { refWsMessage.current = onWsMessage; });
  const refWsClose = useRef(onWsClose);
  useEffect(() => { refWsClose.current = onWsClose; });
  const refWsError = useRef(onWsError);
  useEffect(() => { refWsError.current = onWsError; });

  /* This effect is responsible for triggering a new chat load if a 
   * chat id is not provided in the URL. If the chat is provided
   * in the URL, the backend will create a new chat if necessary
   * during the WebSocket connection sequence.
   * 
   * This should be the only HTTP request made from the chat.
   */
  useEffect(() => {
    if (chatId) {
      console.log(`Chat in use ${chatId}`)
      return;
    }

    console.log(`Requesting new chat.`);

    window.fetch(`${base}/api/v1/chats/`, {
      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 chat server! ` +
          `Try refreshing your browser in a few seconds.`;
        console.error(error);
        setError(error);
        throw new Error(error);
      }
      return res.json();
    }).then((update) => {
      if (update.id !== chatId) {
        console.log(`Chat available: ${update.id}`);
        navigate(`${base}/${update.id}`);
        setChatId(update.id);
      }
    }).catch((error) => {
      console.error(error);
    });
  }, [chatId, setChatId]);

  /* Once a chat id is known, create the sole WebSocket connection
   * to the backend. This WebSocket is then shared with any component
   * that performs chat state updates. Those components should 
   * bind to the 'message:chat-update' WebSocket event and parse
   * their update information from those messages 
   */
  useEffect(() => {
    if (!chatId) {
      return;
    }

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

    console.log(`table - bind`);

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

    if (!ws) {
      return unbind;
    }

    const cbOpen = (e: any) => refWsOpen.current(e);
    const cbMessage = (e: any) => refWsMessage.current(e);
    const cbClose = (e: any) => refWsClose.current(e);
    const cbError = (e: any) => 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, chatId,
    refWsOpen, refWsMessage, refWsClose, refWsError, count, setCount
  ]);

  console.log(`board - (app) - Render with ws: ${ws ? '!' : ''}NULL, connection: ${connection ? '!' : ''}NULL`);

  return <GlobalContext.Provider value={global}>
    { /* <PingPong/> */}
    <div className="Table">
      <div className="Sidebar">
        {name !== "" && <PersonList />}
        {name !== "" && <Chat />}
      </div>
    </div>
  </GlobalContext.Provider>;
};

const App = () => {
  const [personId, setPersonId] = useState<string>('');
  const [error, setError] = useState<string>('');

  useEffect(() => {
    if (personId) {
      return;
    }
    window.fetch(`${base}/api/v1/chats/`, {
      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 chat server! ` +
          `Try refreshing your browser in a few seconds.`;
        console.error(error);
        setError(error);
      }
      console.log(res.headers);
      return res.json();
    }).then((data) => {
      setPersonId(data.person);
    }).catch((error) => {
    });
  }, [personId, setPersonId]);

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

  return (
    <Router>
      <Routes>
        <Route element={<Table />} path={`${base}/:chatId`} />
        <Route element={<Table />} path={`${base}`} />
      </Routes>
    </Router>
  );
}

export default App;
