import {useCallback, useEffect, useRef, useState} from 'react';

import {useClient, usePacket} from '@/react/context/client.js';
import {useDebug} from '@/react/context/debug.js';
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import {useMainEntity} from '@/react/context/main-entity.js';
import {RESOLUTION} from '@/lib/constants.js';
import EventEmitter from '@/lib/event-emitter.js';
import {distribute} from '@/lib/inventory.js';

import addKeyListener from './add-key-listener.js';
import Disconnected from './dom/disconnected.jsx';
import Chat from './dom/chat/chat.jsx';
import Bag from './dom/bag.jsx';
import DateTime from './dom/datetime.jsx';
import Dom from './dom/dom.jsx';
import Entities from './dom/entities.jsx';
import External from './dom/external.jsx';
import Trade from './dom/trade.jsx';
import Wallet from './dom/wallet.jsx';
import HotBar from './dom/hotbar.jsx';
import Pixi from './pixi/pixi.jsx';
import Devtools from './devtools.jsx';
import styles from './ui.module.css';

const KEY_MAP = {
  keyDown: 1,
  keyUp: 0,
};

function emptySlots() {
  return Array(10).fill(undefined);
}

const devEventsChannel = new EventEmitter();

function Ui({disconnected}) {
  // Key input.
  const client = useClient();
  const chatInputRef = useRef();
  const gameRef = useRef();
  const pixiRef = useRef();
  const mainEntityRef = useMainEntity();
  const [, setDebug] = useDebug();
  const ecsRef = useEcs();
  const [showDisconnected, setShowDisconnected] = useState(false);
  const [bufferSlot, setBufferSlot] = useState();
  const hadBufferSlot = useRef();
  const [distributing, setDistributing] = useState({});
  const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false);
  const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y;
  const [camera, setCamera] = useState({x: 0, y: 0});
  const [hotbarSlots, setHotbarSlots] = useState(emptySlots());
  const [inventorySlots, setInventorySlots] = useState(emptySlots());
  const [activeSlot, setActiveSlot] = useState(0);
  const [scale, setScale] = useState(2);
  const monopolizers = useRef([]);
  const [message, setMessage] = useState('');
  const [chatIsOpen, setChatIsOpen] = useState(false);
  const [chatHistory, setChatHistory] = useState([]);
  const [chatHistoryCaret, setChatHistoryCaret] = useState(-1);
  const [chatMessages, setChatMessages] = useState({});
  const [pendingMessage, setPendingMessage] = useState('');
  const [hotbarIsHidden, setHotbarIsHidden] = useState(true);
  const hotbarHideHandle = useRef();
  const [isInventoryOpen, setIsInventoryOpen] = useState(false);
  const [externalInventory, setExternalInventory] = useState();
  const [externalInventorySlots, setExternalInventorySlots] = useState();
  const [gaining, setGaining] = useState([]);
  const [losing, setLosing] = useState([]);
  const [wallet, setWallet] = useState(0);
  const [particleWorker, setParticleWorker] = useState();
  const [trading, setTrading] = useState(false);
  useEffect(() => {
    let handle;
    if (disconnected) {
      handle = setTimeout(() => {
        setShowDisconnected(true);
      }, 1000);
    }
    else {
      setShowDisconnected(false)
    }
    return () => {
      clearTimeout(handle);
    };
  }, [disconnected]);
  const keepHotbarOpen = useCallback(() => {
    if (!isInventoryOpen) {
      setHotbarIsHidden(false);
      if (hotbarHideHandle.current) {
        clearTimeout(hotbarHideHandle.current);
      }
      hotbarHideHandle.current = setTimeout(() => {
        setHotbarIsHidden(true);
      }, 4000);
    }
  }, [isInventoryOpen]);
  useEffect(() => {
    return addKeyListener(document.body, ({event, payload, type}) => {
      const actionMap = {
        'd': {type: 'moveRight'},
        's': {type: 'moveDown'},
        'a': {type: 'moveLeft'},
        'w': {type: 'moveUp'},
        'e': {type: 'interact'},
        '1': {type: 'changeSlot', payload: 1},
        '2': {type: 'changeSlot', payload: 2},
        '3': {type: 'changeSlot', payload: 3},
        '4': {type: 'changeSlot', payload: 4},
        '5': {type: 'changeSlot', payload: 5},
        '6': {type: 'changeSlot', payload: 6},
        '7': {type: 'changeSlot', payload: 7},
        '8': {type: 'changeSlot', payload: 8},
        '9': {type: 'changeSlot', payload: 9},
        '0': {type: 'changeSlot', payload: 10},
        '`': {type: 'openInventory'},
        '-': {type: 'zoomOut'},
        '+': {type: 'zoomIn'},
        '=': {type: 'zoomIn'},
        'F3': {type: 'debug'},
        'F4': {type: 'devtools'},
        'Enter': {type: 'openChat'},
        'Escape': {type: 'closeChat'},
      };
      const action = actionMap[payload];
      if (!action) {
        return;
      }
      if (chatInputRef.current) {
        if ('closeChat' === action.type && 'keyDown' === type) {
          setChatIsOpen(false);
          return;
        }
        chatInputRef.current.focus();
        return;
      }
      let actionPayload;
      switch (action.type) {
        case 'interact': {
          if ('keyDown' === type) {
            if (monopolizers.current.length > 0) {
              monopolizers.current[0].trigger();
              break;
            }
          }
          actionPayload = {type: 'interact', value: KEY_MAP[type]};
          break;
        }
        case 'moveRight':
        case 'moveDown':
        case 'moveLeft':
        case 'moveUp': {
          actionPayload = {type: action.type, value: 'keyDown' === type ? 1 : 0};
          break;
        }
        case 'changeSlot': {
          if ('keyDown' === type) {
            keepHotbarOpen();
            actionPayload = {type: 'changeSlot', value: action.payload};
          }
          break;
        }
        case 'openInventory': {
          if (event) {
            event.preventDefault();
          }
          if ('keyDown' === type) {
            setIsInventoryOpen((isInventoryOpen) => {
              if (isInventoryOpen) {
                setHotbarIsHidden(true);
              }
              else {
                setHotbarIsHidden(false);
                if (hotbarHideHandle.current) {
                  clearTimeout(hotbarHideHandle.current);
                }
              }
              return !isInventoryOpen;
            });
          }
          return;
        }
        case 'zoomIn': {
          if ('keyDown' === type) {
            setScale((scale) => scale < 4 ? Math.floor(scale + 1) : 4);
          }
          return;
        }
        case 'zoomOut': {
          if ('keyDown' === type) {
            setScale((scale) => scale > 1 ? scale - 1 : 1);
          }
          return;
        }
        case 'debug': {
          if (event) {
            event.preventDefault();
          }
          if ('keyDown' === type) {
            setDebug(({...debug}) => ({...debug, info: !debug.info}));
          }
          return;
        }
        case 'devtools': {
          if (event) {
            event.preventDefault();
          }
          if ('keyDown' === type) {
            setDevtoolsIsOpen((devtoolsIsOpen) => !devtoolsIsOpen);
          }
          return;
        }
        case 'openChat': {
          if ('keyDown' === type) {
            setChatIsOpen(true);
          }
          return;
        }
      }
      if (actionPayload) {
        client.send({
          type: 'Action',
          payload: actionPayload,
        });
      }
    });
  }, [client, keepHotbarOpen, mainEntityRef, setDebug]);
  const onEcsChangePacket = useCallback(() => {
    mainEntityRef.current = undefined;
    monopolizers.current = [];
  }, [
    mainEntityRef,
  ]);
  usePacket('EcsChange', onEcsChangePacket);
  const onEcsTick = useCallback((payload, ecs) => {
    for (const id in payload) {
      const entity = ecs.get(id);
      const update = payload[id];
      if (!update) {
        continue;
      }
      if (update.MainEntity) {
        mainEntityRef.current = id;
      }
      if (update.Wallet && mainEntityRef.current === id) {
        setWallet(update.Wallet.gold);
      }
      if (update.Inventory) {
        if (mainEntityRef.current === id) {
          setBufferSlot(entity.Inventory.item(0));
          setHotbarSlots(() => {
            const newHotbarSlots = [];
            for (let i = 1; i < 11; ++i) {
              const item = entity.Inventory.item(i);
              newHotbarSlots.push(
                item
                  ? {
                    icon: item.icon,
                    label: item.label,
                    price: item.price,
                    qty: item.qty,
                  }
                  : undefined,
              );
            }
            return newHotbarSlots;
          });
          const newInventorySlots = emptySlots();
          for (let i = 11; i < 41; ++i) {
            newInventorySlots[i - 11] = entity.Inventory.item(i);
          }
          setInventorySlots(newInventorySlots);
        }
        else if (update.Inventory.slots) {
          const newInventorySlots = Array(30).fill(undefined);
          for (let i = 0; i < 30; ++i) {
            newInventorySlots[i] = entity.Inventory.item(i);
          }
          setExternalInventory(entity.id)
          setTrading(!!entity.Shop);
          setExternalInventorySlots(newInventorySlots);
          setIsInventoryOpen(true);
          setHotbarIsHidden(false);
          if (hotbarHideHandle.current) {
            clearTimeout(hotbarHideHandle.current);
          }
        }
        else if (update.Inventory.closed) {
          setExternalInventory();
          setExternalInventorySlots();
          setGaining([]);
          setLosing([]);
          setTrading(false);
        }
      }
      if (mainEntityRef.current === id) {
        if (update.Wielder && 'activeSlot' in update.Wielder) {
          setActiveSlot(update.Wielder.activeSlot);
        }
      }
    }
  }, [mainEntityRef]);
  useEcsTick(onEcsTick);
  const onEcsTickParticles = useCallback((payload, ecs) => {
    if (!payload[1]?.AreaSize) {
      return;
    }
    if (particleWorker) {
      particleWorker.terminate();
    }
    const localParticleWorker = new Worker(
      new URL('./particle-worker.js', import.meta.url),
      {type: 'module'},
    );
    localParticleWorker.addEventListener('message', () => {
      localParticleWorker.postMessage(ecs.get(1).toJSON());
      setParticleWorker(localParticleWorker);
    });
  }, [particleWorker]);
  useEcsTick(onEcsTickParticles);
  const onEcsTickSound = useCallback((payload) => {
    for (const id in payload) {
      const update = payload[id];
      if (update.Sound?.play) {
        for (const sound of update.Sound.play) {
          (new Audio(sound)).play();
        }
      }
    }
  }, []);
  useEcsTick(onEcsTickSound);
  const onEcsTickAabbs = useCallback((payload, ecs) => {
    for (const id in payload) {
      const entity = ecs.get(id);
      const update = payload[id];
      if (!update) {
        continue;
      }
      if (update.Direction && entity.Collider) {
        entity.Collider.updateAabbs();
      }
    }
  }, []);
  useEcsTick(onEcsTickAabbs);
  const onEcsTickCamera = useCallback((payload) => {
    if (mainEntityRef.current && payload[mainEntityRef.current]?.Camera) {
      setCamera((camera) => ({
        x: camera.x,
        y: camera.y,
        ...payload[mainEntityRef.current].Camera,
      }))
    }
  }, [mainEntityRef]);
  useEcsTick(onEcsTickCamera);
  useEffect(() => {
    function onContextMenu(event) {
      event.preventDefault();
    }
    document.body.addEventListener('contextmenu', onContextMenu);
    return () => {
      document.body.removeEventListener('contextmenu', onContextMenu);
    };
  }, []);
  const computePosition = useCallback(({clientX, clientY}) => {
    if (!gameRef.current || !mainEntityRef.current || !ecsRef.current) {
      return;
    }
    const {top, left, width} = gameRef.current.getBoundingClientRect();
    const master = ecsRef.current.get(1);
    if (!master) {
      return;
    }
    const {Camera} = ecsRef.current.get(mainEntityRef.current);
    const size = width / RESOLUTION.x;
    const camera = {
      x: ((Camera.x * scale) - (RESOLUTION.x / 2)),
      y: ((Camera.y * scale) - (RESOLUTION.y / 2)),
    }
    return {
      x: (((clientX - left) / size) + camera.x) / scale,
      y: (((clientY - top) / size) + camera.y) / scale,
    };
  }, [
    ecsRef,
    mainEntityRef,
    scale,
  ]);
  const hotbarOnSlotMouseDown = useCallback((i, event) => {
    keepHotbarOpen();
    if (trading) {
      const index = losing.indexOf(i + 1);
      if (-1 === index) {
        losing.push(i + 1);
      }
      else {
        losing.splice(index, 1);
      }
      setLosing([...losing]);
    }
    else {
      hadBufferSlot.current = bufferSlot;
      switch (event.button) {
        case 0:
          if (bufferSlot) {
            // ...
          }
          else {
            client.send({
              type: 'Action',
              payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 1]},
            });
          }
          break;
        case 2:
          client.send({
            type: 'Action',
            payload: {
              type: 'distribute',
              value: [
                i + 1,
                [
                  [mainEntityRef.current, 0],
                  [mainEntityRef.current, i + 1],
                ],
              ],
            },
          });
          break;
      }
    }
  }, [bufferSlot, client, keepHotbarOpen, losing, mainEntityRef, trading]);
  const hotbarOnSlotMouseMove = useCallback((i) => {
    if (hadBufferSlot.current) {
      setDistributing((distributing) => ({
        ...distributing,
        [[mainEntityRef.current, i + 1].join(':')]: true,
      }));
    }
  }, [mainEntityRef]);
  useEffect(() => {
    if (!bufferSlot) {
      return;
    }
    const entity = ecsRef.current.get(mainEntityRef.current);
    setHotbarSlots(() => {
      const newHotbarSlots = [];
      for (let i = 1; i < 11; ++i) {
        const item = entity.Inventory.item(i);
        newHotbarSlots.push(
          item
            ? {icon: item.icon, qty: item.qty}
            : undefined,
        );
      }
      const qtys = [];
      for (const key in distributing) {
        const [entityId, slot] = key.split(':');
        const {Inventory} = ecsRef.current.get(entityId);
        qtys.push(Inventory.slots[slot]?.qty ?? 0);
      }
      const item = {
        maximumStack: bufferSlot.maximumStack,
        qty: bufferSlot.qty,
      };
      const distributed = distribute(item, qtys);
      let i = 0;
      for (const key in distributing) {
        const [entityId, slot] = key.split(':');
        if (
          entityId == mainEntityRef.current
          && (slot >= 1 && slot <= 11)
        ) {
          if (!newHotbarSlots[slot - 1]) {
            newHotbarSlots[slot - 1] = {
              icon: bufferSlot.icon,
            };
          }
          newHotbarSlots[slot - 1].qty = distributed[i];
          newHotbarSlots[slot - 1].temporary = true;
        }
        i += 1;
      }
      return newHotbarSlots;
    });
  }, [bufferSlot, distributing, ecsRef, mainEntityRef])
  const hotbarOnSlotMouseUp = useCallback((i, event) => {
    keepHotbarOpen();
    if (trading) {
      // ...
    }
    else {
      switch (event.button) {
        case 0:
          if (Object.keys(distributing).length > 0) {
            client.send({
              type: 'Action',
              payload: {
                type: 'distribute',
                value: [
                  0,
                  Object.keys(distributing).map((idAndSlot) => idAndSlot.split(':')),
                ],
              },
            });
            setDistributing({});
          }
          else if (hadBufferSlot.current) {
            client.send({
              type: 'Action',
              payload: {
                type: 'distribute',
                value: [
                  0,
                  [
                    [mainEntityRef.current, i + 1],
                  ],
                ],
              },
            });
          }
          else {
            // ...
          }
          break;
      }
    }
    hadBufferSlot.current = undefined;
  }, [client, distributing, keepHotbarOpen, mainEntityRef, trading]);
  const bagOnSlotMouseDown = useCallback((i) => {
    if (trading) {
      const index = losing.indexOf(i + 11);
      if (-1 === index) {
        losing.push(i + 11);
      }
      else {
        losing.splice(index, 1);
      }
      setLosing([...losing]);
    }
    else {
      client.send({
        type: 'Action',
        payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 11]},
      });
    }
  }, [client, losing, mainEntityRef, trading]);
  const externalInventoryOnSlotMouseDown = useCallback((i) => {
    if (trading) {
      const index = gaining.indexOf(i);
      if (-1 === index) {
        gaining.push(i);
      }
      else {
        gaining.splice(index, 1);
      }
      setGaining([...gaining]);
    }
    else {
      client.send({
        type: 'Action',
        payload: {type: 'swapSlots', value: [0, externalInventory, i]},
      });
    }
  }, [client, externalInventory, gaining, trading]);
  const onTradeAccepted = useCallback(() => {
    client.send({
      type: 'Action',
      payload: {type: 'acceptTrade', value: {gaining, losing}},
    });
    setGaining([]);
    setLosing([]);
  }, [client, gaining, losing]);
  useEffect(() => {
    if (!pixiRef.current) {
      return;
    }
    pixiRef.current.scale.set(scale);
    pixiRef.current.x = -Math.round((camera.x * scale) - RESOLUTION.x / 2);
    pixiRef.current.y = -Math.round((camera.y * scale) - RESOLUTION.y / 2);
  }, [camera, scale])
  return (
    <div
      className={styles.ui}
    >
      <style>
        {`
          @media (max-aspect-ratio: ${ratio}) { .${styles.game}, .${styles.ui} { width: 100%; } }
          @media (min-aspect-ratio: ${ratio}) { .${styles.game}, .${styles.ui} { height: 100%; } }
          .${styles.game} {
            cursor: ${
              bufferSlot
                ? `url('${bufferSlot.icon}'), auto !important`
                : 'auto'
              };
          }
        `}
      </style>
      {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */ }
      <div
        className={[styles.game, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}
        onMouseDown={(event) => {
          if (chatInputRef.current) {
            event.preventDefault();
            return;
          }
          const where = computePosition(event);
          switch (event.button) {
            case 0:
              if (devtoolsIsOpen) {
                devEventsChannel.invoke(
                  'click',
                  where,
                  event,
                );
              }
              else if (!isInventoryOpen) {
                client.send({
                  type: 'Action',
                  payload: {type: 'use', value: [1, where]},
                });
              }
              break;
            case 2:
              if (monopolizers.current.length > 0) {
                monopolizers.current[0].trigger();
                break;
              }
              client.send({
                type: 'Action',
                payload: {type: 'interact', value: 1},
              });
              break;
          }
        }}
        onMouseUp={(event) => {
          if (chatInputRef.current) {
            event.preventDefault();
            return;
          }
          const where = computePosition(event);
          switch (event.button) {
            case 0:
              client.send({
                type: 'Action',
                payload: {type: 'use', value: [0, where]},
              });
              break;
            case 2:
              client.send({
                type: 'Action',
                payload: {type: 'interact', value: 0},
              });
              break;
          }
          event.preventDefault();
        }}
        onWheel={(event) => {
          if (chatInputRef.current) {
            event.preventDefault();
            return;
          }
          if (!isInventoryOpen) {
            setHotbarIsHidden(false);
            if (hotbarHideHandle.current) {
              clearTimeout(hotbarHideHandle.current);
            }
            hotbarHideHandle.current = setTimeout(() => {
              setHotbarIsHidden(true);
            }, 4000);
          }
          if (event.deltaY > 0) {
            client.send({
              type: 'Action',
              payload: {type: 'changeSlot', value: 1 + ((activeSlot + 1) % 10)},
            });
          }
          else {
            client.send({
              type: 'Action',
              payload: {type: 'changeSlot', value: 1 + ((activeSlot + 9) % 10)},
            });
          }
        }}
        ref={gameRef}
      >
        <Pixi
          monopolizers={monopolizers.current}
          particleWorker={particleWorker}
          ref={pixiRef}
        />
        <Dom>
          <HotBar
            active={activeSlot}
            highlighted={trading && losing.filter((i) => i < 11).map((i) => i - 1)}
            hotbarIsHidden={hotbarIsHidden}
            onSlotMouseDown={hotbarOnSlotMouseDown}
            onSlotMouseMove={hotbarOnSlotMouseMove}
            onSlotMouseUp={hotbarOnSlotMouseUp}
            slots={hotbarSlots}
          />
          <Bag
            highlighted={trading && losing.filter((i) => i >= 11).map((i) => i - 11)}
            isInventoryOpen={isInventoryOpen}
            onSlotMouseDown={bagOnSlotMouseDown}
            slots={inventorySlots}
          />
          {externalInventory && (
            <div className={styles.external}>
              <External
                highlighted={trading && gaining}
                isInventoryOpen={isInventoryOpen}
                onSlotMouseDown={externalInventoryOnSlotMouseDown}
                slots={externalInventorySlots}
              />
              {trading && (
                <Trade
                  isInventoryOpen={isInventoryOpen}
                  onTradeAccepted={onTradeAccepted}
                  gaining={gaining.map((slot) => externalInventorySlots[slot])}
                  losing={losing.map((slot) => {
                    return slot < 11 ? hotbarSlots[slot - 1] : inventorySlots[slot - 11];
                  })}
                  wallet={wallet}
                />
              )}
            </div>
          )}
          <Entities
            camera={camera}
            scale={scale}
            setChatMessages={setChatMessages}
            monopolizers={monopolizers.current}
          />
          {chatIsOpen && (
            <Chat
              chatHistory={chatHistory}
              chatHistoryCaret={chatHistoryCaret}
              chatInputRef={chatInputRef}
              chatMessages={chatMessages}
              message={message}
              pendingMessage={pendingMessage}
              setChatHistory={setChatHistory}
              setChatHistoryCaret={setChatHistoryCaret}
              setMessage={setMessage}
              setPendingMessage={setPendingMessage}
            />
          )}
          {showDisconnected && (
            <Disconnected />
          )}
          <DateTime />
          <Wallet gold={wallet} />
        </Dom>
      </div>
      {devtoolsIsOpen && (
        <div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
          <Devtools
            eventsChannel={devEventsChannel}
          />
        </div>
      )}
    </div>
  );
}

export default Ui;
