import { RendererApi } from '../common/rendererApi';
import { BlocklyApp } from './blockly-app';
import hljs from 'highlight.js';
import * as impl from './impl';

// DOMの取得
function throwError(msg: string): never {
  throw new Error(msg);
}
const consoleDom = document.getElementById('console') ?? throwError('console Not Found');
const blocklyApp = new BlocklyApp('blockly-area', require('./toolbox.xml?raw'));
const cameraImgDom = <HTMLImageElement>document.getElementById('camera-img') ?? throwError('console Not Found');

function escapeHTML(text: string): string {
  const map: { [key: string]: string } = {
    '<': '&lt;',
    '>': '&gt;',
    '&': '&amp;',
  };
  return text.replace(/[<>&]/g, x => map[x]);
}
// APIの実装
export const api: RendererApi = {
  highlight(id, on) {
    blocklyApp.setHighlight(id, on);
  },

  writeImage(base64) {
    cameraImgDom.src = 'data:image/jpeg;base64,' + base64;
  },

  cmd(c: string, args?: string[]): void {
    let text = c;
    if (args) {
      for (const a of args) {
        text += ' ' + a;
      }
    }
    consoleDom.innerHTML += `<p class='command'>${escapeHTML(text)}</p>`;
    consoleDom.scrollTop = consoleDom.scrollHeight;
  },

  writeLine(text: string): void {
    if (!text) return;
    consoleDom.innerHTML += `<p class='stdout'>${escapeHTML(text)}</p>`;
  },
  writeLineErr(text: string): void {
    if (!text) return;
    consoleDom.innerHTML += `<p class='stderr'>${escapeHTML(text)}</p>`;
    consoleDom.scrollTop = consoleDom.scrollHeight;
  },
  err(text: string): void {
    if (!text) return;
    consoleDom.innerHTML += `<p class='error'>${escapeHTML(text)}</p>`;
    consoleDom.scrollTop = consoleDom.scrollHeight;
  },
  log(text: string): void {
    if (!text) return;
    consoleDom.innerHTML += `<p class='log'>${escapeHTML(text)}</p>`;
    consoleDom.scrollTop = consoleDom.scrollHeight;
  },
  clearConsole(): void {
    consoleDom.innerHTML = '';
  },
};
app.register(api);

// イベントハンドラ
const runDom = <HTMLInputElement>document.getElementById('run') ?? throwError('Run Button Not Found');
runDom.addEventListener('click', async () => {
  if (runDom.disabled) return;
  runDom.disabled = true;
  api.clearConsole();
  try {
    // requestPortは必ず最初に呼び出してください
    // 何らかのawaitの後に実行するとセキュリティ制限にかかります
    if (!(await requestPort())) return;
    await impl.runAction(blocklyApp.getCode(), '', api, app.main, app.os);
    impl.openPort(api);
  } finally {
    runDom.disabled = false;
  }
});

let defaultFilePath = 'Hello.xml';
document.getElementById('save')?.addEventListener('click', async () => {
  const savePath = await app.main.showSaveFileDialog({
    defaultPath: defaultFilePath,
    filters: [{ name: 'XML', extensions: ['xml'] }],
  });
  if (!savePath) return;
  defaultFilePath = savePath;
  const text = blocklyApp.getWorkspaceText();
  await app.main.saveTextFile(savePath, text);
});

document.getElementById('load')?.addEventListener('click', async () => {
  const loadPath = await app.main.showOpenFileDialog({
    defaultPath: defaultFilePath,
    filters: [{ name: 'XML', extensions: ['xml'] }],
  });
  if (!loadPath) return;
  const data = await app.main.readTextFile(loadPath);
  if (data === null) return;
  defaultFilePath = loadPath;
  blocklyApp.setWorkspaceText(data);
});

const blockBtnDom = <HTMLInputElement>document.getElementById('block-button') ?? throwError('blockly button not found');
const cppAreaDom = document.getElementById('cpp-area') ?? throwError('C++ area Not Found');

import 'highlight.js/styles/default.css'; // highlight.jsのCSS の読み込み
/// highlight.js を用いてコードハイライトを行います
function highlightCode(code: string) {
  const html = hljs.highlight(code, { language: 'cpp' }).value;
  cppAreaDom.getElementsByTagName('code')[0].innerHTML = html;
}
let currentMode: 'block' | 'code' = 'block';
document.getElementsByName('mode-change-button').forEach(e => {
  e.addEventListener('click', () => {
    const nextMode = blockBtnDom.checked ? 'block' : 'code';
    if (currentMode == nextMode) return;
    currentMode = nextMode;
    switch (currentMode) {
      case 'block':
        blocklyApp.setVisible(true);
        cppAreaDom.style.display = 'none';
        break;
      case 'code':
        highlightCode(blocklyApp.getCode());
        blocklyApp.setVisible(false);
        cppAreaDom.style.display = 'block';
        break;
    }
  });
});

// ポート選択
const portDom = <HTMLSelectElement>document.getElementById('port') ?? throwError('port dropdown is not found');
async function updatePortList(): Promise<void> {
  const ports = await app.main.getPorts();
  const options = portDom.options;
  const old = portDom.selectedOptions.item(0);
  let oldSelected: string | null = null;
  if (old) {
    oldSelected = `${old.getAttribute('product') ?? ''};${old.getAttribute('vendor') ?? ''}`;
  }
  for (let i = options.length - 1; i >= 0; i--) {
    options.remove(i);
  }
  let oldFound = false;
  let selectIndex = -1;
  let i = 0;
  for (const p of ports) {
    const value = `${p.productId};${p.vendorId}`;
    if (value == oldSelected) {
      selectIndex = i;
      oldFound = true;
    }
    const item = document.createElement('option');
    item.setAttribute('product', p.productId);
    item.setAttribute('vendor', p.vendorId);
    item.innerText = p.port;
    options.add(item);
    i++;
  }
  if (selectIndex != -1) portDom.selectedIndex = selectIndex;
  if (old && !oldFound) {
    return impl.closePort();
  }
}

/**
 * シリアルポートを開くための要求をブラウザに出します。
 * セキュリティ制限により、ユーザーイベントによるハンドラから
 * 直接起動する必要があります。
 * await に注意。
 */
async function requestPort(): Promise<boolean> {
  const selected = portDom.selectedOptions.item(0);
  if (!selected) {
    api.err('ポートを選択してください。');
    return false;
  }
  const productIdStr = selected.getAttribute('product') ?? '';
  const vendorIdStr = selected.getAttribute('vendor') ?? '';
  let productId: number | undefined = parseInt(productIdStr, 16);
  let vendorId: number | undefined = parseInt(vendorIdStr, 16);
  if (isNaN(productId)) productId = undefined;
  if (isNaN(vendorId)) vendorId = undefined;
  return await impl.requestPort(api, selected.innerText, productId, vendorId);
}

updatePortList();
navigator.serial.addEventListener('connect', updatePortList);
navigator.serial.addEventListener('disconnect', updatePortList);
portDom.addEventListener('click', updatePortList);
// navigator.serial.getPorts();
// ↑セキュリティに引っかかって動作はしないが、connectとdisconnectが何故か反応するようになる
