/**************************************************************************************
# 076 License

Copyright (c) 2026 テクニカル諏訪子

Permission is hereby granted to any person obtaining a copy of the software
Hexagon (the "Software") to use, modify, merge, copy, publish, distribute,
sublicense, and/or sell copies of the Software, subject to the following conditions:

    1. **Origin Attribution**:
       - You must not misrepresent the origin of the Software; you must not claim
         you created the original Software.
       - If the Software is used in a product, you must either:
         a. Provide clear attribution in the product's documentation, user interface,
            or other visible areas, **OR**
         b. Pay the original developers a fee they specify in writing.
    2. **Usage Restriction**:
       - The Software, or any derivative works, dependencies, or libraries
         incorporating it, must not be used for censorship or to suppress freedom of
         speech, expression, or creativity. Prohibited uses include, but are not
         limited to:
         - Censorship of so-called "hate speech", visuals, non-mainstream opinions,
           ideas, or objective reality.
         - Tools or systems designed to restrict access to information or
           artistic works.
    3. **Notice Preservation**:
       - This license and the above copyright notice must remain intact in all copies
         of the source code.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
**************************************************************************************/
#include <algorithm>
#include <csignal>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <set>
#include <sstream>

#include <locale.h>
#include <wchar.h>
#ifdef _WIN32
#include <windows.h>
#endif

#include "hexeditor.hh"

#ifdef _WIN32
static BOOL WINAPI ConsoleCtrlHandler(DWORD dwCtrlType) {
  if (dwCtrlType == CTRL_C_EVENT) return TRUE;
  return FALSE;
}
#endif

const std::vector<HexEditor::FileSignature> HexEditor::signatures = {
  {{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, "PNG"},
  {{ 0xFF, 0xD8 }, "JPEG"},
  {{ 0x7F, 0x45, 0x4C, 0x46 }, "ELF"},
  {{ 0x4D, 0x5A }, "PE"},
  {{ 0x25, 0x50, 0x44, 0x46 }, "PDF"},
  {{ 0x67, 0x69, 0x6D, 0x70, 0x20, 0x78, 0x63, 0x66 }, "GIMP XCF"},
  {{ 0x1A, 0x45, 0xDF, 0xA3, 0x9F, 0x42, 0x86, 0x81, 0x01, 0x42,
     0xF7, 0x81, 0x01, 0x42, 0xF2, 0x81, 0x04, 0x42, 0xF3, 0x81,
     0x08, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6D, 0x42, 0x87,
     0x81, 0x04, 0x42, 0x85, 0x81, 0x02, 0x18, 0x53, 0x80, 0x67,
     0x01, 0x00, 0x00, 0x00, 0x01 }, "WEBM"},
  {{ 0x1A, 0x45, 0xDF, 0xA3, 0xA3, 0x42, 0x86, 0x81, 0x01, 0x42,
     0xF7, 0x81, 0x01, 0x42, 0xF2, 0x81, 0x04, 0x42, 0xF3, 0x81,
     0x08, 0x42, 0x82, 0x88, 0x6D, 0x61, 0x74, 0x72, 0x6F, 0x73,
     0x6B, 0x61, 0x42, 0x87, 0x81, 0x04, 0x42, 0x85, 0x81, 0x02,
     0x18, 0x53, 0x80, 0x67, 0x01, 0x00, 0x00, 0x00}, "Matroska"},
  {{ 0x52, 0x49, 0x46, 0x46 }, "Waveform"},
};

HexEditor::HexEditor(const std::string &filename)
  : curPos(0), dpOffset(0), bpr(16),
    statusMode(Status_Normal), modified(false),
    running(true), hasFile(!filename.empty()),
    lastSearchDir(Direction_Forward),
    headerLen(0), headerType("") {
  // SIGINT (CTRL + C)を無効に
#ifdef _WIN32
  SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE);
#else
  std::signal(SIGINT, SIG_IGN);
#endif

  setlocale(LC_ALL, "");

  // ncurses
  initscr();
#ifdef _WIN32
  HWND console = GetConsoleWindow();
  if (console) {
    LONG style = GetWindowLong(console, GWL_STYLE);
    style |= WS_MAXIMIZEBOX | WS_THICKFRAME;
    SetWindowLong(console, GWL_STYLE, style);

    const int MIN_WIDTH = 100;
    const int MIN_HEIGHT = 30;

    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    GetConsoleScreenBufferInfo(hOut, &csbi);

    RECT rect;
    GetWindowRect(console, &rect);

    int cw = csbi.srWindow.Right - csbi.srWindow.Left + 1;
    int ch = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;

    MINMAXINFO mmi = {};
    mmi.ptMinTrackSize.x = MIN_WIDTH * 8;
    mmi.ptMinTrackSize.y = MIN_HEIGHT * 16;

    COORD minBuf = { MIN_WIDTH, MIN_HEIGHT };
    SetConsoleScreenBufferSize(hOut, minBuf);

    SMALL_RECT rectWin = { 0, 0, MIN_WIDTH - 1, MIN_HEIGHT - 1 };
    SetConsoleWindowInfo(hOut, TRUE, &rectWin);
  }

  resize_term(40, 120);
#endif
  getmaxyx(stdscr, rows, cols);
  clearok(stdscr, TRUE);
  cbreak();
  noecho();
  keypad(stdscr, TRUE);
  raw();
  start_color();

  // マウス対応
#ifdef _WIN32
  mousemask(ALL_MOUSE_EVENTS, nullptr);
  mouse_set(ALL_MOUSE_EVENTS);
  request_mouse_pos();
#else
  mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, nullptr);
#endif
  isMouse = true;

  init_pair(1, COLOR_BLACK, COLOR_WHITE); // カーソル
  init_pair(2, COLOR_BLACK, COLOR_GREEN); // 変更
  init_pair(3, COLOR_BLACK, COLOR_MAGENTA); // 普通
  init_pair(4, COLOR_BLACK, COLOR_CYAN); // コマンド
  init_pair(5, COLOR_BLACK, COLOR_YELLOW); // 検索
  init_pair(6, COLOR_BLACK, COLOR_YELLOW); // 検索ハイライト
  init_pair(7, COLOR_BLACK, COLOR_RED); // エラー
  init_pair(8, COLOR_WHITE, COLOR_BLACK); // デフォルト
  init_pair(9, COLOR_CYAN,  COLOR_BLACK); // ファイルヘッダー
  refresh();

  // ファイルをバッファーに読み込む
  if (hasFile) {
    std::ifstream file(filename, std::ios::binary);
    if (!file) {
      endwin();
      throw std::runtime_error("ファイルを開くに失敗");
    }

    buf.assign((std::istreambuf_iterator<char>(file)), {});
    file.close();

    if (buf.empty()) {
      endwin();
      throw std::runtime_error("ファイルが空です");
    }

    fname = filename;
    hasFile = true;
  
    // ファイルヘッダー
    for (const auto &sig : signatures) {
      if (buf.size() >= sig.signature.size()) {
        bool match = true;
        for (size_t i = 0; i < sig.signature.size(); ++i) {
          if (buf[i] != sig.signature[i]) {
            match = false;
            break;
          }
        }
      
        if (match) {
          headerLen = sig.signature.size();
          headerType = sig.type;
          break;
        }
      }
    }
  } else {
    fname = "[No File]";
    buf.clear();
    hasFile = false;
  }

  // ウィンドウを作成する
  getmaxyx(stdscr, rows, cols);
  if (rows < 4 || cols < 20) {
    endwin();
    throw std::runtime_error("ターミナルが小さ過ぎます");
  }

  bpr = std::min<size_t>(16, (cols / 2 - 10) / 4);
  hexPanel = newwin(rows - 3, cols / 2, 1, 0);
  asciiPanel = newwin(rows - 3, cols / 2, 1, cols / 2);
  scrollok(hexPanel, TRUE);
  scrollok(asciiPanel, TRUE);
  wattrset(hexPanel, COLOR_PAIR(8));
  wattrset(asciiPanel, COLOR_PAIR(8));
}

HexEditor::~HexEditor() {
  delwin(hexPanel);
  delwin(asciiPanel);
  endwin();
}

void HexEditor::highlightcol(size_t i, size_t row, uint8_t byte) {
  wattron(hexPanel, COLOR_PAIR(1));
  mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
  wattroff(hexPanel, COLOR_PAIR(1));

  wattron(asciiPanel, COLOR_PAIR(1));
  mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
  wattroff(asciiPanel, COLOR_PAIR(1));
}

void HexEditor::topbar() {
  int colorPair = 3;
  std::string status = "Hexagon 1.2.0";
  if (!headerType.empty()) status += " | FILETYPE: " + headerType;

  std::wstring wstatus(status.begin(), status.end());
  wattron(stdscr, COLOR_PAIR(colorPair));
  mvprintw(0, 0, "%s", status.c_str());
  for (int i = status.length(); i < cols; ++i) {
    mvaddch(0, i, ' ');
  }

  wattroff(stdscr, COLOR_PAIR(colorPair));
}

void HexEditor::statusbar() {
  int colorPair;
  switch (statusMode) {
    case Status_Replace: colorPair = 2; break;
    case Status_Normal: colorPair = 3; break;
    case Status_Command: colorPair = 4; break;
    case Status_Search: colorPair = 5; break;
    case Status_Error: colorPair = 7; break;
  }

  std::string status;
  if (statusMode == Status_Normal || statusMode == Status_Error) {
    status = "FILE: " + fname;
    status += " | OFFSET: 0x" + std::to_string(curPos);
    status += " | FILE SIZE: " + std::to_string(buf.size()) + " B";
    if (!lastSearch.empty()) {
      status += " | SEARCH: " + lastSearch;
    } else if (!lastHexSearch.empty()) {
      std::ostringstream hexSearch;
      for (size_t i = 0; i < lastHexSearch.size(); ++i) {
        if (i > 0) hexSearch << " ";
        hexSearch << std::hex << std::setfill('0') << std::setw(2)
          << (int)lastHexSearch[i];
      }
      status += " | SEARCH: " + hexSearch.str();
    }
    status += (modified ? " [+]" : "");
    if (statusMode == Status_Error) status += " | ERROR: " + statusText;
    else status += !statusText.empty() ? " | " + statusText : "";
  } else if (statusMode == Status_Command) {
    status = ":" + statusText;
  } else if (statusMode == Status_Search) {
    status = (lastSearchDir == Direction_Forward ? "/" : "?") + statusText;
  } else if (statusMode == Status_Replace) {
    status = "-- REPLACE --";
  }

  std::wstring wstatus(status.begin(), status.end());
  wattron(stdscr, COLOR_PAIR(colorPair));
  mvprintw(rows - 1, 0, "%s", status.c_str());
  for (int i = status.length(); i < cols; ++i) {
    mvaddch(rows - 1, i, ' ');
  }

  wattroff(stdscr, COLOR_PAIR(colorPair));
}

void HexEditor::render() {
  werase(hexPanel);
  werase(asciiPanel);

  getmaxyx(stdscr, rows, cols);
  size_t maxRows = rows - 3;

  topbar();

  std::set<size_t> matchBytes;
  if (!lastSearch.empty()) {
    for (size_t i = 0; i <= buf.size() - lastSearch.size(); ++i) {
      bool match = true;
      for (size_t j = 0; j < lastSearch.size(); ++j) {
        if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
          match = false;
          break;
        }
      }

      if (match) {
        for (size_t j = 0; j < lastSearch.size(); ++j) {
          matchBytes.insert(i + j);
        }
      }
    }
  } else if (!lastHexSearch.empty()) {
    for (size_t i = 0; i <= buf.size() - lastHexSearch.size(); ++i) {
      bool match = true;
      for (size_t j = 0; j < lastHexSearch.size(); ++j) {
        if (buf[i + j] != lastHexSearch[j]) {
          match = false;
          break;
        }
      }

      if (match) {
        for (size_t j = 0; j < lastHexSearch.size(); ++j) {
          matchBytes.insert(i + j);
        }
      }
    }
  }

  // HEXとASCIIの表示
  for (size_t row = 0; row < maxRows; ++row) {
    size_t offset = dpOffset + row * bpr;
    if (offset >= buf.size()) break;

    std::ostringstream hexLine, asciiLine;
    hexLine << std::hex << std::setfill('0') << std::setw(8) << offset << ": ";
    mvwprintw(hexPanel, row, 0, "%s", hexLine.str().c_str());

    // HEXとASCIIのデータ
    for (size_t i = 0; i < bpr && (offset + i) < buf.size(); ++i) {
      uint8_t byte = buf[offset + i];
      bool isMatch = matchBytes.count(offset + i) > 0;
      bool isHeader = (offset + i) < headerLen;

      if (offset + i == curPos) {
        highlightcol(i, row, byte);
      } else if (isHeader) {
        wattron(hexPanel, COLOR_PAIR(9));
        mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
        wattroff(hexPanel, COLOR_PAIR(9));

        wattron(asciiPanel, COLOR_PAIR(9));
        mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
        wattroff(asciiPanel, COLOR_PAIR(9));
      } else if (isMatch) {
        wattron(hexPanel, COLOR_PAIR(6));
        mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
        wattroff(hexPanel, COLOR_PAIR(6));

        wattron(asciiPanel, COLOR_PAIR(6));
        mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
        wattroff(asciiPanel, COLOR_PAIR(6));
      } else {
        wattron(hexPanel, COLOR_PAIR(8));
        mvwprintw(hexPanel, row, 10 + i * 3, "%02x", byte);
        wattroff(hexPanel, COLOR_PAIR(8));

        wattron(asciiPanel, COLOR_PAIR(8));
        mvwaddch(asciiPanel, row, i, std::isprint(byte) ? byte : '.');
        wattroff(asciiPanel, COLOR_PAIR(8));
      }
    }
  }

  statusbar();

  redrawwin(hexPanel);
  redrawwin(asciiPanel);
  wrefresh(hexPanel);
  wrefresh(asciiPanel);
  refresh();
}

void HexEditor::findNextMatch() {
  if (lastSearch.empty() && lastHexSearch.empty()) return;

  size_t searchSize = lastSearch.empty() ? lastHexSearch.size() : lastSearch.size();
  size_t startPos = curPos + 1;

  for (size_t i = startPos; i <= buf.size() - searchSize; ++i) {
    bool match = true;
    if (!lastSearch.empty()) {
      for (size_t j = 0; j < lastSearch.size(); ++j) {
        if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
          match = false;
          break;
        }
      }
    } else {
      for (size_t j = 0; j < lastHexSearch.size(); ++j) {
        if (buf[i + j] != lastHexSearch[j]) {
          match = false;
          break;
        }
      }
    }
    if (match) {
      curPos = i;
      dpOffset = curPos - (curPos % bpr);
      render();
      return;
    }
  }

  for (size_t i = 0; i < startPos && i <= buf.size() - searchSize; ++i) {
    bool match = true;
    if (!lastSearch.empty()) {
      for (size_t j = 0; j < lastSearch.size(); ++j) {
        if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
          match = false;
          break;
        }
      }
    } else {
      for (size_t j = 0; j < lastHexSearch.size(); ++j) {
        if (buf[i + j] != lastHexSearch[j]) {
          match = false;
          break;
        }
      }
    }
    if (match) {
      curPos = i;
      dpOffset = curPos - (curPos % bpr);
      render();
      return;
    }
  }
}

void HexEditor::findPrevMatch() {
  if (lastSearch.empty() && lastHexSearch.empty()) return;

  size_t searchSize = lastSearch.empty() ? lastHexSearch.size() : lastSearch.size();
  size_t startPos = curPos > 0 ? curPos - 1 : buf.size() - searchSize;

  for (size_t i = startPos + 1; i > 0; --i) {
    size_t pos = i - 1;
    if (pos > buf.size() - searchSize) continue;
    bool match = true;
    if (!lastSearch.empty()) {
      for (size_t j = 0; j < lastSearch.size(); ++j) {
        if (buf[pos + j] != static_cast<uint8_t>(lastSearch[j])) {
          match = false;
          break;
        }
      }
    } else {
      for (size_t j = 0; j < lastHexSearch.size(); ++j) {
        if (buf[pos + j] != lastHexSearch[j]) {
          match = false;
          break;
        }
      }
    }
    if (match) {
      curPos = pos;
      dpOffset = curPos - (curPos % bpr);
      render();
      return;
    }
  }

  for (size_t i = buf.size() - searchSize; i > startPos; --i) {
    bool match = true;
    if (!lastSearch.empty()) {
      for (size_t j = 0; j < lastSearch.size(); ++j) {
        if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
          match = false;
          break;
        }
      }
    } else {
      for (size_t j = 0; j < lastHexSearch.size(); ++j) {
        if (buf[i + j] != lastHexSearch[j]) {
          match = false;
          break;
        }
      }
    }
    if (match) {
      curPos = i;
      dpOffset = curPos - (curPos % bpr);
      render();
      return;
    }
  }
}

void HexEditor::handleCommand() {
  statusMode = Status_Command;
  statusText.clear();
  render();

  int ch;
  while ((ch = getch()) != '\n' && ch != 27) {
#ifdef _WIN32
    if (ch == KEY_BACKSPACE || ch == 8) {
#else
    if (ch == KEY_BACKSPACE || ch == 127) {
#endif
      if (!statusText.empty()) {
        statusText.pop_back();
      }
    } else if (ch >= 32 && ch <= 126) { // 書き込める文字
      statusText += ch;
    }

    render();
  }

  if (ch == 27) { // Esc
    statusMode = Status_Normal;
    statusText.clear();
    render();
    return;
  }

  bool isCommand = false;
  if (statusText == "w") {
    handleSave();
    isCommand = true;
  } else if (statusText == "q") {
    handleQuit(false);
    isCommand = true;
  } else if (statusText == "wq") {
    handleSave();
    handleQuit(false);
    isCommand = true;
  } else if (statusText == "q!") {
    handleQuit(true);
    isCommand = true;
  } else if (statusText == "wq!") {
    handleSave();
    handleQuit(true);
    isCommand = true;
  } else if (statusText == "noh") {
    lastSearch = "";
    isCommand = true;
  } else if (statusText.rfind("o ", 0) == 0 || statusText.rfind("open ", 0) == 0) {
    std::string path = statusText.substr(statusText.find(' ') + 1);
    handleOpen(path);
    isCommand = true;
  } else {
    statusMode = Status_Error;
    statusText = "不正なコマンド。";
  }

  if (isCommand) {
    statusMode = Status_Normal;
    statusText.clear();
  }
  render();
}

void HexEditor::handleQuit(bool force) {
  if (force || (!force && !modified)) {
    running = false;
  } else {
    statusMode = Status_Error;
    statusText.clear();
    render();
  }
}

void HexEditor::handleOpen(const std::string &path) {
  if (path.empty()) {
    statusMode = Status_Error;
    statusText = "利用方法： :o <filename>";
    render();
    return;
  }

  std::ifstream file(path, std::ios::binary);
  if (!file) {
    statusMode = Status_Error;
    statusText = "ファイルを開くに失敗： " + path;
    render();
    return;
  }

  std::vector<uint8_t> newbuf((std::istreambuf_iterator<char>(file)), {});
  file.close();

  if (newbuf.empty()) {
    statusMode = Status_Error;
    statusText = "ファイルは空です。";
    render();
    return;
  }

  buf = std::move(newbuf);
  fname = path;
  hasFile = true;
  curPos = 0;
  dpOffset = 0;
  modified = false;
  undoStack.clear();
  redoStack.clear();
  lastSearch.clear();
  lastHexSearch.clear();
  headerLen = 0;
  headerType = "";

  for (const auto &sig : signatures) {
    if (buf.size() >= sig.signature.size()) {
      bool match = true;
      for (size_t i = 0; i < sig.signature.size(); ++i) {
        if (buf[i] != sig.signature[i]) {
          match = false;
          break;
        }
      }
      if (match) {
        headerLen = sig.signature.size();
        headerType = sig.type;
        break;
      }
    }
  }

  statusMode = Status_Normal;
  statusText = path + " を開きました。";
  render();
}

void HexEditor::handleSave() {
  std::ofstream file(fname, std::ios::binary);
  if (file) {
    file.write(reinterpret_cast<const char *>(buf.data()), buf.size());
    file.close();
    modified = false;
    statusText = "保存済み";
  }
}

void HexEditor::handleSearch() {
  statusMode = Status_Search;
  lastSearchDir = Direction_Forward;
  statusText.clear();
  render();

  int ch;
  while ((ch = getch()) != '\n' && ch != 27) {
#ifdef _WIN32
    if (ch == KEY_BACKSPACE || ch == 8) {
#else
    if (ch == KEY_BACKSPACE || ch == 127) {
#endif
      if (!statusText.empty()) statusText.pop_back();
    } else if (ch >= 32 && ch <= 126) { // 書き込める文字
      statusText += ch;
    }

    render();
  }

  if (ch == 27) { // Esc
    statusMode = Status_Normal;
    statusText.clear();
    render();
    return;
  }

  lastSearch.clear();
  lastHexSearch.clear();
  matchOff.clear();

  std::istringstream iss(statusText);
  std::string hexByte;
  bool isHexSearch = true;
  std::vector<uint8_t> hexSearch;
  while (iss >> hexByte) {
    if (hexByte.size() != 2 || !std::all_of(hexByte.begin(), hexByte.end(), [](char c) { return std::isxdigit(c); })) {
      isHexSearch = false;
      break;
    }
    try {
      hexSearch.push_back(static_cast<uint8_t>(std::stoul(hexByte, nullptr, 16)));
    } catch (...) {
      isHexSearch = false;
      break;
    }
  }

  if (isHexSearch && !hexSearch.empty()) { // HEX検索
    lastHexSearch = hexSearch;
    for (size_t i = curPos + 1; i <= buf.size() - lastHexSearch.size(); ++i) {
      bool match = true;
      for (size_t j = 0; j < lastHexSearch.size(); ++j) {
        if (buf[i + j] != lastHexSearch[j]) {
          match = false;
          break;
        }
      }
      if (match) {
        matchOff.push_back(i);
        curPos = i;
        dpOffset = curPos - (curPos % bpr);
        break;
      }
    }
  } else { // ASCII検索
    lastSearch = statusText;
    for (size_t i = curPos + 1; i <= buf.size() - lastSearch.size(); ++i) {
      bool match = true;
      for (size_t j = 0; j < lastSearch.size(); ++j) {
        if (buf[i + j] != static_cast<uint8_t>(lastSearch[j])) {
          match = false;
          break;
        }
      }
      if (match) {
        matchOff.push_back(i);
        curPos = i;
        dpOffset = curPos - (curPos % bpr);
        break;
      }
    }
  }

  statusMode = Status_Normal;
  statusText.clear();
  render();
}

void HexEditor::handleReverseSearch() {
  statusMode = Status_Search;
  lastSearchDir = Direction_Reverse;
  statusText.clear();
  render();

  int ch;
  while ((ch = getch()) != '\n' && ch != 27) {
#ifdef _WIN32
    if (ch == KEY_BACKSPACE || ch == 8) {
#else
    if (ch == KEY_BACKSPACE || ch == 127) {
#endif
      if (!statusText.empty()) statusText.pop_back();
    } else if (ch >= 32 && ch <= 126) {
      statusText += ch;
    }
    render();
  }

  if (ch == 27) { // Esc
    statusMode = Status_Normal;
    statusText.clear();
    render();
    return;
  }

  lastSearch.clear();
  lastHexSearch.clear();
  matchOff.clear();

  std::istringstream iss(statusText);
  std::string hexByte;
  bool isHexSearch = true;
  std::vector<uint8_t> hexSearch;
  while (iss >> hexByte) {
    if (hexByte.size() != 2 || !std::all_of(hexByte.begin(), hexByte.end(), [](char c) { return std::isxdigit(c); })) {
      isHexSearch = false;
      break;
    }
    try {
      hexSearch.push_back(static_cast<uint8_t>(std::stoul(hexByte, nullptr, 16)));
    } catch (...) {
      isHexSearch = false;
      break;
    }
  }

  if (isHexSearch && !hexSearch.empty()) {
    lastHexSearch = hexSearch;
    size_t startPos = curPos > 0 ? curPos - 1 : buf.size() - lastHexSearch.size();
    for (size_t i = startPos + 1; i > 0; --i) {
      size_t pos = i - 1;
      if (pos > buf.size() - lastHexSearch.size()) continue;
      bool match = true;
      for (size_t j = 0; j < lastHexSearch.size(); ++j) {
        if (buf[pos + j] != lastHexSearch[j]) {
          match = false;
          break;
        }
      }
      if (match) {
        matchOff.push_back(pos);
        curPos = pos;
        dpOffset = curPos - (curPos % bpr);
        break;
      }
    }
  } else {
    lastSearch = statusText;
    size_t startPos = curPos > 0 ? curPos - 1 : buf.size() - lastSearch.size();
    for (size_t i = startPos + 1; i > 0; --i) {
      size_t pos = i - 1;
      if (pos > buf.size() - lastSearch.size()) continue;
      bool match = true;
      for (size_t j = 0; j < lastSearch.size(); ++j) {
        if (buf[pos + j] != static_cast<uint8_t>(lastSearch[j])) {
          match = false;
          break;
        }
      }
      if (match) {
        matchOff.push_back(pos);
        curPos = pos;
        dpOffset = curPos - (curPos % bpr);
        break;
      }
    }
  }

  statusMode = Status_Normal;
  statusText.clear();
  render();
}

void HexEditor::handleReplace() {
  statusMode = Status_Replace;
  statusText.clear();
  render();

  std::string hexInput;
  int ch;
  while ((ch = getch()) != 27) {
    if (std::isxdigit(ch) && hexInput.size() < 2) {
      hexInput += ch;
      statusText = "REPLACE: " + hexInput;
      render();
    }

    if (hexInput.size() == 2) {
      uint8_t byte = std::stoul(hexInput, nullptr, 16);
      if (curPos < buf.size()) {
        undoStack.push_back({curPos, buf[curPos], byte});
        redoStack.clear();
        buf[curPos] = byte;
        modified = true;
        curPos = std::min(curPos + 1, buf.size() - 1);
      }

      hexInput.clear();
      statusText.clear();
      render();
    }
  }

  statusMode = Status_Normal;
  statusText.clear();
  render();
}

void HexEditor::handleMouse() {
#ifdef _WIN32
  request_mouse_pos();

  int mx = Mouse_status.x;
  int my = Mouse_status.y;

  bool click = false;
  for (int i = 0; i < 3; ++i) {
    if ((Mouse_status.button[i] & BUTTON1_PRESSED)
    || (Mouse_status.button[i] & BUTTON1_CLICKED)) {
      click = true;
      break;
    }
  }
  if (!click) return;
#else
  MEVENT event;
  if (getmouse(&event) != OK) return;
  if (!(event.bstate & (BUTTON1_PRESSED | BUTTON1_CLICKED))) return;

  int mx = event.x;
  int my = event.y;
#endif
  if (my == 0 || my == rows - 1) return;

  if (my >= 1 && my < rows - 2 && mx < cols / 2) {
    // HEXパネル
    int panelRow = my - 1;
    int panelCol = mx;
    if (panelCol < 10) return;

    int i = (panelCol - 10) / 3;
    size_t offset = dpOffset + panelRow * bpr + i;
    if (offset < buf.size()) {
      curPos = offset;
      render();
    }
  } else if (my >= 1 && my < rows - 2 && mx >= cols / 2) {
    // ASCIIパネル
    int panelRow = my - 1;
    int i = mx - cols / 2;
    size_t offset = dpOffset + panelRow * bpr + i;
    if (offset < buf.size()) {
      curPos = offset;
      render();
    }
  }
}

void HexEditor::undo() {
  if (undoStack.empty()) {
    statusMode = Status_Error;
    statusText = "既に一番古い変更です";
    render();
    return;
  }

  Edit edit = undoStack.back();
  undoStack.pop_back();
  redoStack.push_back({edit.offset, edit.newByte, edit.oldByte});
  buf[edit.offset] = edit.oldByte;
  if (undoStack.empty()) modified = false;
  else modified = true;
  curPos = edit.offset;
  dpOffset = curPos - (curPos % bpr);
  render();
}

void HexEditor::redo() {
  if (redoStack.empty()) {
    statusMode = Status_Error;
    statusText = "既に一番新しい変更です";
    render();
    return;
  }

  Edit edit = redoStack.back();
  redoStack.pop_back();
  undoStack.push_back({edit.offset, edit.newByte, edit.oldByte});
  buf[edit.offset] = edit.newByte;
  modified = true;
  curPos = edit.offset;
  dpOffset = curPos - (curPos % bpr);
  render();
}

void HexEditor::input() {
  int ch;

  while (running) {
    if (statusMode != Status_Normal && statusMode != Status_Error) continue;

    ch = getch();

    if (ch == KEY_RESIZE) {
      getmaxyx(stdscr, rows, cols);
      delwin(hexPanel);
      delwin(asciiPanel);

      bpr = std::min<size_t>(16, (cols / 2 - 10) / 4);
      if (bpr < 4) bpr = 4;

      hexPanel = newwin(rows - 3, cols / 2, 1, 0);
      asciiPanel = newwin(rows - 3, cols / 2, 1, cols / 2);

      scrollok(hexPanel, TRUE);
      scrollok(asciiPanel, TRUE);
      wattrset(hexPanel, COLOR_PAIR(8));
      wattrset(asciiPanel, COLOR_PAIR(8));

      render();
      continue;
    }

    if (!hasFile && ch != ':' && ch != 'Z') {
      statusMode = Status_Error;
      statusText = "ファイルを読み込んでいません。「:o <file>」を御利用下さい。";
      render();
      continue;
    }

    if ((ch == 'j' || ch == KEY_DOWN) && curPos + bpr < buf.size()) {
      curPos += bpr; // 下
      if (statusMode == Status_Error) statusMode = Status_Normal;
    } else if ((ch == 'k' || ch == KEY_UP) && curPos >= bpr) {
      curPos -= bpr; // 上
      if (statusMode == Status_Error) statusMode = Status_Normal;
    } else if ((ch == 'l' || ch == KEY_RIGHT) && curPos + 1 < buf.size()) {
      curPos += 1; // 右
      if (statusMode == Status_Error) statusMode = Status_Normal;
    } else if ((ch == 'h' || ch == KEY_LEFT) && curPos > 0) {
      curPos -= 1; // 左
      if (statusMode == Status_Error) statusMode = Status_Normal;
    } else if ((ch == 'J' || ch == KEY_PPAGE) && curPos >= bpr) {
      size_t pageSize = (rows - 3) * bpr;
      if (curPos >= pageSize) curPos -= pageSize;
      else curPos = 0;
      if (statusMode == Status_Error) statusMode = Status_Normal;
    } else if ((ch == 'K' || ch == KEY_NPAGE) && curPos + bpr < buf.size()) {
      size_t pageSize = (rows - 3) * bpr;
      if (curPos + pageSize < buf.size()) {
        curPos += pageSize;
        dpOffset = curPos - ((rows - 4) * bpr);
      }
      else {
        curPos = buf.size() - 1;
        dpOffset = (buf.size() - 1) - ((rows - 4) * bpr);
        if (dpOffset > buf.size()) dpOffset = 0;
      }
      if (statusMode == Status_Error) statusMode = Status_Normal;
    } else if (ch == '^' || ch == '0' || ch == KEY_HOME) {
      curPos = (curPos / bpr) * bpr;
      if (statusMode == Status_Error) statusMode = Status_Normal;
    } else if (ch == '$' || ch == KEY_END) {
      size_t rowStart = (curPos / bpr) * bpr;
      curPos = std::min(rowStart + bpr - 1, buf.size() - 1);
      if (statusMode == Status_Error) statusMode = Status_Normal;
    } else if (ch == 'i') {
      if (curPos - 1 > 0) curPos--;
      handleReplace();
    } else if (ch == 'r') {
      handleReplace();
    } else if (ch == 'a') {
      if (curPos + 1 < buf.size()) curPos++;
      handleReplace();
    } else if (ch == ':') {
      handleCommand();
    } else if (ch == '/') {
      handleSearch();
    } else if (ch == '?') {
      handleReverseSearch();
    } else if (ch == 'n') {
      if (lastSearchDir == Direction_Forward) findNextMatch();
      else findPrevMatch();
    } else if (ch == 'N') {
      if (lastSearchDir == Direction_Forward) findPrevMatch();
      else findNextMatch();
    } else if (ch == 'u') {
      undo();
    } else if (ch == 'R') {
      redo();
    } else if (ch == 'g') {
      if (getch() == 'g') {
        curPos = 0; // ファイルの一番上
        if (statusMode == Status_Error) statusMode = Status_Normal;
      }
    } else if (ch == 'G') {
      curPos = buf.size() - 1; // ファイルの一番下
      if (statusMode == Status_Error) statusMode = Status_Normal;
    } else if (ch == 'Z') {
      int next = getch();
      if (next == 'Q') {
        handleQuit(true);
        break;
      } else if (next == 'Z') {
        handleSave();
        handleQuit(true);
        break;
      } else if (next == 'S') {
        handleSave();
        if (statusMode == Status_Error) statusMode = Status_Normal;
      }
    } else if (ch == KEY_MOUSE) {
      handleMouse();
      continue;
    }

    // 画面の動き
    int rows, cols;
    getmaxyx(stdscr, rows, cols);
    if (curPos < dpOffset) {
      dpOffset = curPos - (curPos % bpr);
    } else if (curPos >= dpOffset + (rows - 3) * bpr) {
      dpOffset = curPos - ((rows - 4) * bpr);
    }

    render();
  }
}

void HexEditor::run() {
  render();
  input();
}
