const decoder = new TextDecoder();

let socket = null;
let lineBuffer = '';
let commandQueue = Promise.resolve();

const lineListeners = new Set();

function emitLine(line) {
  for (const listener of lineListeners) {
    listener(line);
  }
}

function parseIncomingChunk(chunk) {
  lineBuffer += chunk;

  while (true) {
    const delimiterIndex = lineBuffer.indexOf('\r\n');
    if (delimiterIndex < 0) {
      break;
    }

    const line = lineBuffer.slice(0, delimiterIndex);
    lineBuffer = lineBuffer.slice(delimiterIndex + 2);
    emitLine(line);
  }
}

function handleSocketMessage(event) {
  if (typeof event.data === 'string') {
    parseIncomingChunk(event.data);
    return;
  }

  if (event.data instanceof ArrayBuffer) {
    parseIncomingChunk(decoder.decode(event.data));
  }
}

function normalizeCommand(command) {
  return command.endsWith('\r') ? command : `${command}\r`;
}

function enqueueCommand(fn) {
  const run = commandQueue.then(fn, fn);
  commandQueue = run.catch(() => {});
  return run;
}

function ensureSocketReady() {
  if (!socket || socket.readyState !== WebSocket.OPEN) {
    throw new Error('EggDuino WiFi socket is not connected');
  }
}

function buildSocketUrl(options) {
  if (options.url) {
    return options.url;
  }

  const host = options.host;
  if (!host) {
    throw new Error('Missing EggDuino host');
  }

  const secure = options.secure === true;
  const protocol = secure ? 'wss' : 'ws';
  const port = Number(options.port ?? 1337);
  const path = options.path ?? '/';

  return `${protocol}://${host}:${port}${path}`;
}

export async function connectEggDuinoWifi(options = {}) {
  if (socket && socket.readyState === WebSocket.OPEN) {
    return;
  }

  if (socket && socket.readyState === WebSocket.CONNECTING) {
    await new Promise((resolve, reject) => {
      const onOpen = () => {
        socket.removeEventListener('error', onError);
        resolve();
      };
      const onError = () => {
        socket.removeEventListener('open', onOpen);
        reject(new Error('EggDuino WiFi socket failed while connecting'));
      };
      socket.addEventListener('open', onOpen, { once: true });
      socket.addEventListener('error', onError, { once: true });
    });
    return;
  }

  const url = buildSocketUrl(options);
  socket = new WebSocket(url);
  socket.binaryType = 'arraybuffer';

  socket.addEventListener('message', handleSocketMessage);
  socket.addEventListener('close', () => {
    socket = null;
  });

  await new Promise((resolve, reject) => {
    const onOpen = () => {
      socket.removeEventListener('error', onError);
      resolve();
    };

    const onError = () => {
      socket.removeEventListener('open', onOpen);
      socket?.close();
      socket = null;
      reject(new Error(`EggDuino WiFi socket connection failed: ${url}`));
    };

    socket.addEventListener('open', onOpen, { once: true });
    socket.addEventListener('error', onError, { once: true });
  });
}

export function disconnectEggDuinoWifi() {
  if (!socket) {
    return;
  }

  socket.removeEventListener('message', handleSocketMessage);
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
    socket.close();
  }
  socket = null;
}

export async function sendEggBotCommand(command) {
  ensureSocketReady();
  socket.send(normalizeCommand(command));
}

export async function sendEggBotCommandExpectOk(command, timeoutMs = 2000) {
  return enqueueCommand(() => new Promise(async (resolve, reject) => {
    const collectedLines = [];

    const timeout = setTimeout(() => {
      lineListeners.delete(onResponseLine);
      reject(new Error('EggBot response timeout'));
    }, timeoutMs);

    const onResponseLine = (line) => {
      collectedLines.push(line);

      if (line === 'OK') {
        clearTimeout(timeout);
        lineListeners.delete(onResponseLine);
        resolve(collectedLines.slice(0, -1));
      }

      if (line === 'unknown CMD') {
        clearTimeout(timeout);
        lineListeners.delete(onResponseLine);
        reject(new Error(`EggBot error: ${collectedLines.join(' | ')}`));
      }
    };

    lineListeners.add(onResponseLine);

    try {
      await sendEggBotCommand(command);
    } catch (error) {
      clearTimeout(timeout);
      lineListeners.delete(onResponseLine);
      reject(error);
    }
  }));
}

export function onLine(callback) {
  lineListeners.add(callback);
}

export function offLine(callback) {
  lineListeners.delete(callback);
}
