#include <Wire.h>
#include "unihiker_k10.h"
#include "src/DFRobot_HumanPose/DFRobot_HumanPose.h"
#include "src/DFRobot_SpeechSynthesis_M/DFRobot_SpeechSynthesis_M.h"

class HumanPose400K : public DFRobot_HumanPose_I2C {
public:
  HumanPose400K(TwoWire *wire, uint8_t address)
    : DFRobot_HumanPose_I2C(wire, address) {}
  bool begin() {
    _wire->begin();
    _wire->setClock(400000);
    _wait_delay = 2;
    return DFRobot_HumanPose::begin();
  }
};

// -------------------- 硬件与屏幕配置 --------------------
const uint8_t SENSOR_ADDR = 0x3A;
const uint8_t SPEECH_ADDR = 0x40;
const uint32_t GESTURE_I2C_CLOCK = 400000;
const uint32_t SPEECH_I2C_CLOCK = 100000;
const uint16_t SPEECH_ACK_POLL_MS = 60;
const uint32_t SPEECH_MIN_TIMEOUT_MS = 5000;
const uint32_t SPEECH_MAX_TIMEOUT_MS = 20000;

// 方向 1 为 320x240 横屏；若实机画面上下颠倒，将 1 改为 3。
const uint8_t SCREEN_DIR = 1;
const int16_t SCREEN_W = 320;
const int16_t SCREEN_H = 240;
const int16_t FOOTER_Y = 202;

// 普通数字灯模块：高电平点亮；低电平有效的模块可改成 LOW。
const uint8_t GESTURE_LED_PIN = P0; // 开发板定义 P0 = GPIO1
const uint8_t GESTURE_LED_ON = HIGH;
const uint16_t GESTURE_LED_PULSE_MS = 200;
bool gestureLedLit = false;
unsigned long gestureLedSince = 0;

void flashGestureLed() {
  digitalWrite(GESTURE_LED_PIN, GESTURE_LED_ON);
  gestureLedSince = millis();
  gestureLedLit = true;
}

void updateGestureLed() {
  if (gestureLedLit && millis() - gestureLedSince >= GESTURE_LED_PULSE_MS) {
    digitalWrite(GESTURE_LED_PIN, GESTURE_LED_ON == HIGH ? LOW : HIGH);
    gestureLedLit = false;
  }
}

// -------------------- 界面配色 --------------------
const uint32_t COLOR_BG          = 0xFFF9EF;
const uint32_t COLOR_CARD        = 0xFFFFFB;
const uint32_t COLOR_TEXT        = 0x2D1A0E;
const uint32_t COLOR_MUTED       = 0x76665A;
const uint32_t COLOR_ORANGE      = 0xFF8A24;
const uint32_t COLOR_ORANGE_DARK = 0xE56E0A;
const uint32_t COLOR_ORANGE_PALE = 0xFFF0DE;
const uint32_t COLOR_GREEN       = 0x5E9B32;
const uint32_t COLOR_GREEN_PALE  = 0xEEF5D9;
const uint32_t COLOR_LINE        = 0xEAD8C1;
const uint32_t COLOR_WHITE       = 0xFFFFFF;
const uint32_t COLOR_RED         = 0xD94A38;

// 程序内部操作编号，必须经 mapGestureResult 映射，不能直接使用传感器原始 ID。
const uint8_t GESTURE_LEFT  = 1; // 左摆：自学习 ID1
const uint8_t GESTURE_RIGHT = 2; // 右摆：自学习 ID2
const uint8_t GESTURE_OK    = 3; // 固定模型原始 ID8 映射到此操作
const uint8_t GESTURE_FIST  = 4; // 固定模型原始 ID0 映射到此操作
const uint8_t GESTURE_NONE  = 0;

// -------------------- 菜谱数据 --------------------
struct Recipe {
  const char *name;
  const char *category;
  const char *totalTime;
  const char *ingredients[5];
  const char *steps[5];
  const char *stepTips[5];
  uint16_t stepSeconds[5];
  uint8_t stepCount;
};

const Recipe RECIPES[] = {
  {
    "西红柿炒鸡蛋", "家常菜", "10分钟",
    {"西红柿：2个", "鸡蛋：3个", "盐：适量", "糖：适量", "食用油：适量"},
    {"鸡蛋加盐打散", "西红柿洗净切块", "鸡蛋炒熟后盛出", "炒西红柿至出汁", "倒入鸡蛋调味装盘"},
    {"搅拌至蛋清和蛋黄均匀", "切成大小均匀的小块", "观察鸡蛋凝固状态", "中火翻炒并轻压出汁", "翻炒均匀即可关火"},
    {0, 0, 0, 0, 0},
    5
  },
  {
    "蛋炒饭", "快手菜", "8分钟",
    {"隔夜米饭：1碗", "鸡蛋：2个", "火腿：适量", "葱花：适量", "盐、生抽：适量"},
    {"米饭提前打散", "鸡蛋炒散后盛出", "火腿丁下锅翻炒", "加入米饭炒散", "加鸡蛋葱花调味"},
    {"把结块米饭轻轻拨散", "炒至凝固且保持嫩滑", "小火炒出香味", "快速翻炒至颗粒分明", "翻炒均匀后即可装盘"},
    {0, 0, 0, 0, 0},
    5
  },
  {
    "可乐鸡翅", "荤菜", "30分钟",
    {"鸡翅：8个", "可乐：1罐", "姜片：适量", "葱段：适量", "生抽、料酒：适量"},
    {"鸡翅洗净两面划刀", "冷水下锅焯水", "煎鸡翅至两面金黄", "加调料和可乐焖煮", "大火收汁后装盘"},
    {"每面划两刀方便入味", "撇去浮沫后捞出沥干", "中小火慢煎避免焦糊", "保持小火并盖上锅盖", "不断翻动避免粘锅"},
    {0, 0, 0, 900, 0},
    5
  },
  {
    "青椒炒肉", "家常菜", "12分钟",
    {"瘦肉：200克", "青椒：2个", "蒜：2瓣", "生抽、淀粉：适量", "盐、食用油：适量"},
    {"瘦肉切片并腌制", "青椒洗净切块", "肉片下锅快速滑炒", "加入青椒和蒜末", "调味翻炒后装盘"},
    {"加生抽和淀粉抓匀", "去籽后切成均匀小块", "肉片变色后先盛出", "大火翻炒至青椒断生", "倒回肉片炒匀即可"},
    {0, 0, 0, 0, 0},
    5
  }
};

const uint8_t RECIPE_COUNT = sizeof(RECIPES) / sizeof(RECIPES[0]);
const uint8_t VISIBLE_RECIPE_COUNT = 3;

// 与 RECIPES 顺序一致；TF 卡文件名保持不变，步骤编号从 1 开始。
const char *const RECIPE_IMAGE_PREFIXES[] = {
  "recipe_tomato_", "recipe_fried_rice_",
  "recipe_cola_wings_", "recipe_pepper_pork_"
};
static_assert(sizeof(RECIPE_IMAGE_PREFIXES) / sizeof(RECIPE_IMAGE_PREFIXES[0])
              == RECIPE_COUNT, "Recipe image mapping must match recipes");
// 保留路径字符串，供画布图片接口使用。
String stepImagePath;

enum PageState {
  PAGE_RECIPE_LIST,
  PAGE_INGREDIENTS,
  PAGE_COOKING_STEP,
  PAGE_FINISHED
};

UNIHIKER_K10 k10;
HumanPose400K sensor(&Wire, SENSOR_ADDR);
DFRobot_SpeechSynthesis_I2C speech(&Wire, SPEECH_ADDR);
bool speechReady = false;

PageState currentPage = PAGE_RECIPE_LIST;
uint8_t selectedRecipe = 0;
uint8_t currentStep = 0;
uint8_t recipeWindowStart = 0;

bool gestureLocked = false;
uint8_t candidateGesture = GESTURE_NONE;
unsigned long candidateSince = 0;
unsigned long lastGestureSeen = 0;

bool timerRunning = false;
bool timerFinished = false;
uint16_t remainingSeconds = 0;
unsigned long lastTimerTick = 0;
bool timerWaitingForSpeech = false;

bool speechPlaying = false;
unsigned long nextSpeechAckPoll = 0;
unsigned long speechDeadline = 0;

void restoreGestureI2CClock() {
  Wire.setClock(GESTURE_I2C_CLOCK);
}

// 将 UTF-8 文本转换为 V1 模块所需的 UTF-16LE 字节。
uint16_t encodeUtf16LE(const String &text, uint8_t *output,
                       uint16_t capacity, uint16_t &characterCount) {
  const uint8_t *input = (const uint8_t *)text.c_str();
  uint16_t inputLength = text.length();
  uint16_t inputIndex = 0;
  uint16_t outputLength = 0;
  characterCount = 0;
  while (inputIndex < inputLength) {
    uint32_t codePoint = '?';
    uint8_t first = input[inputIndex];
    if (first < 0x80) {
      codePoint = first;
      inputIndex++;
    } else if ((first & 0xE0) == 0xC0 && inputIndex + 1 < inputLength) {
      codePoint = ((uint32_t)(first & 0x1F) << 6) |
                  (input[inputIndex + 1] & 0x3F);
      inputIndex += 2;
    } else if ((first & 0xF0) == 0xE0 && inputIndex + 2 < inputLength) {
      codePoint = ((uint32_t)(first & 0x0F) << 12) |
                  ((uint32_t)(input[inputIndex + 1] & 0x3F) << 6) |
                  (input[inputIndex + 2] & 0x3F);
      inputIndex += 3;
    } else if ((first & 0xF8) == 0xF0 && inputIndex + 3 < inputLength) {
      codePoint = ((uint32_t)(first & 0x07) << 18) |
                  ((uint32_t)(input[inputIndex + 1] & 0x3F) << 12) |
                  ((uint32_t)(input[inputIndex + 2] & 0x3F) << 6) |
                  (input[inputIndex + 3] & 0x3F);
      inputIndex += 4;
    } else {
      inputIndex++;
    }
    if (codePoint <= 0xFFFF) {
      if (outputLength + 2 > capacity) break;
      output[outputLength++] = codePoint & 0xFF;
      output[outputLength++] = (codePoint >> 8) & 0xFF;
    } else {
      if (outputLength + 4 > capacity) break;
      codePoint -= 0x10000;
      uint16_t highSurrogate = 0xD800 | (codePoint >> 10);
      uint16_t lowSurrogate = 0xDC00 | (codePoint & 0x3FF);
      output[outputLength++] = highSurrogate & 0xFF;
      output[outputLength++] = highSurrogate >> 8;
      output[outputLength++] = lowSurrogate & 0xFF;
      output[outputLength++] = lowSurrogate >> 8;
    }
    characterCount++;
  }
  return outputLength;
}

bool writeSpeechI2CBlock(const uint8_t *data, uint16_t length) {
  Wire.beginTransmission(SPEECH_ADDR);
  for (uint16_t i = 0; i < length; i++) Wire.write(data[i]);
  return Wire.endTransmission() == 0;
}

// 只发送合成指令，不调用语音库中没有超时的 wait()。
bool startSpeechAsync(const String &text) {
  uint16_t capacity = text.length() * 2 + 4;
  uint8_t *payload = (uint8_t *)malloc(capacity);
  if (payload == NULL) {
    Serial.println(F("[Speech] UTF-16 buffer allocation failed."));
    return false;
  }
  uint16_t characterCount = 0;
  uint16_t payloadLength = encodeUtf16LE(
    text, payload, capacity, characterCount);
  if (payloadLength == 0) {
    free(payload);
    return false;
  }
  // V1 协议：0xFD + 长度 + 合成指令 0x01 + Unicode 编码 0x03。
  uint16_t frameLength = payloadLength + 2;
  uint8_t header[5] = {
    0xFD,
    (uint8_t)(frameLength >> 8),
    (uint8_t)(frameLength & 0xFF),
    0x01,
    0x03
  };
  Wire.setClock(SPEECH_I2C_CLOCK);
  bool sent = writeSpeechI2CBlock(header, sizeof(header));
  for (uint16_t offset = 0; sent && offset < payloadLength; offset += 28) {
    uint16_t blockLength = payloadLength - offset;
    if (blockLength > 28) blockLength = 28;
    sent = writeSpeechI2CBlock(payload + offset, blockLength);
  }
  free(payload);
  restoreGestureI2CClock();
  if (!sent) {
    Serial.println(F("[Speech] I2C send failed; announcement skipped."));
    return false;
  }
  unsigned long now = millis();
  // 语速 5 下按字数估算一个保守的播放时长，仅用于应答丢失时兜底。
  uint32_t timeoutMs = 2000UL + (uint32_t)characterCount * 350UL;
  if (timeoutMs < SPEECH_MIN_TIMEOUT_MS) timeoutMs = SPEECH_MIN_TIMEOUT_MS;
  if (timeoutMs > SPEECH_MAX_TIMEOUT_MS) timeoutMs = SPEECH_MAX_TIMEOUT_MS;
  speechPlaying = true;
  nextSpeechAckPoll = now + SPEECH_ACK_POLL_MS;
  speechDeadline = now + timeoutMs;
  return true;
}

void finishSpeech(bool timedOut) {
  speechPlaying = false;
  restoreGestureI2CClock();
  Serial.println(timedOut
                   ? F("[Speech] completion ACK timeout; program continues.")
                   : F("[Speech] playback completed."));
  if (timerWaitingForSpeech) {
    timerWaitingForSpeech = false;
    if (currentPage == PAGE_COOKING_STEP &&
        remainingSeconds > 0 && !timerFinished) {
      timerRunning = true;
      lastTimerTick = millis();
      Serial.println(F("[Timer] countdown started after speech."));
    }
  }
}

void updateSpeech() {
  if (!speechPlaying) return;
  unsigned long now = millis();
  if ((long)(now - nextSpeechAckPoll) >= 0) {
    nextSpeechAckPoll = now + SPEECH_ACK_POLL_MS;
    Wire.setClock(SPEECH_I2C_CLOCK);
    Wire.requestFrom((uint8_t)SPEECH_ADDR, (uint8_t)2);
    bool playbackCompleted = false;
    while (Wire.available()) {
      if ((uint8_t)Wire.read() == 0x4F) playbackCompleted = true;
    }
    restoreGestureI2CClock();
    if (playbackCompleted) {
      finishSpeech(false);
      return;
    }
  }
  if ((long)(now - speechDeadline) >= 0) finishSpeech(true);
}

bool speakText(const String &text) {
  if (!speechReady) {
    Serial.println(F("Speech skipped: module is not ready."));
    return false;
  }
  if (speechPlaying) {
    Serial.println(F("Speech skipped: previous announcement is still active."));
    return false;
  }
  Serial.print(F("Speech: "));
  Serial.println(text);
  return startSpeechAsync(text);
}

// -------------------- 文本与基础绘图工具 --------------------
// 统一查找下一个 UTF-8 字符边界，供测宽、居中和底栏文字复用。
uint16_t nextCharacter(const String &text, uint16_t index) {
  do { ++index; }
  while (index < text.length() && ((uint8_t)text[index] & 0xC0) == 0x80);
  return index;
}

int16_t utf8TextWidth(const String &text, uint8_t chineseWidth, uint8_t asciiWidth) {
  int16_t width = 0;
  for (uint16_t i = 0; i < text.length(); i = nextCharacter(text, i))
    width += (uint8_t)text[i] < 0x80 ? asciiWidth : chineseWidth;
  return width;
}

void drawText16(const String &text, int16_t x, int16_t y, uint32_t color,
                int count = 30) {
  k10.canvas->canvasText(text, x, y, color,
                         k10.canvas->eCNAndENFont16, count, false);
}

void drawText24(const String &text, int16_t x, int16_t y, uint32_t color,
                int count = 20) {
  k10.canvas->canvasText(text, x, y, color,
                         k10.canvas->eCNAndENFont24, count, false);
}

void drawCentered16(const String &text, int16_t centerX, int16_t y, uint32_t color) {
  drawText16(text, centerX - utf8TextWidth(text, 16, 8) / 2, y, color);
}

void drawCentered24(const String &text, int16_t centerX, int16_t y, uint32_t color) {
  drawText24(text, centerX - utf8TextWidth(text, 24, 12) / 2, y, color);
}

// 将单行或多行文字作为整体，在指定矩形内水平、垂直居中。
void drawTextBox(const String &text, int16_t x, int16_t y,
                 int16_t w, int16_t h, uint32_t color, bool large) {
  const int16_t fontH = large ? 24 : 16;
  String lines[8];
  uint8_t row = 0;
  int16_t used = 0;
  for (uint16_t i = 0; i < text.length();) {
    uint16_t end = nextCharacter(text, i);
    int16_t gw = (uint8_t)text[i] < 0x80 ? fontH / 2 : fontH;
    if (used + gw > w && used > 0) {
      if (row == 7) break;
      row++;
      used = 0;
    }
    lines[row] += text.substring(i, end);
    used += gw;
    i = end;
  }
  int16_t top = y + (h - (row + 1) * fontH) / 2;
  for (uint8_t n = 0; n <= row; n++) {
    int16_t left = x + (w - utf8TextWidth(lines[n], fontH, fontH / 2)) / 2;
    if (large) drawText24(lines[n], left, top + n * fontH, color);
    else drawText16(lines[n], left, top + n * fontH, color);
  }
}

void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color) {
  k10.canvas->canvasRectangle(x, y, w, h, color, color, true);
}

void outlineRect(int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color) {
  k10.canvas->canvasRectangle(x, y, w, h, color, color, false);
}

void beginPage() {
  k10.canvas->canvasClear();
  fillRect(0, 0, SCREEN_W, SCREEN_H, COLOR_BG);
}

void drawHeader(const String &status = "手势") {
  fillRect(0, 0, SCREEN_W, 39, COLOR_CARD);
  k10.canvas->canvasSetLineWidth(1);
  k10.canvas->canvasLine(0, 38, SCREEN_W, 38, COLOR_ORANGE_PALE);
  // 简化的厨师帽图标。
  k10.canvas->canvasCircle(16, 14, 6, COLOR_ORANGE, COLOR_BG, false);
  k10.canvas->canvasCircle(23, 12, 7, COLOR_ORANGE, COLOR_BG, false);
  k10.canvas->canvasCircle(30, 14, 6, COLOR_ORANGE, COLOR_BG, false);
  outlineRect(17, 17, 12, 9, COLOR_ORANGE);
  drawText24("电子菜谱助手", 40, 7, COLOR_TEXT);
  // 状态圆点和文字使用同一中心线，文字从圆点右侧固定位置开始。
  // 实机16像素字体的可见笔画偏上，向下补偿2像素进行视觉对齐。
  const int16_t statusCenterY = 19;
  const int16_t statusTextOffsetY = 2;
  const int16_t statusDotX = 239;
  const int16_t statusTextX = statusDotX + 4 + 6;
  k10.canvas->canvasCircle(statusDotX, statusCenterY, 4,
                           COLOR_GREEN, COLOR_GREEN, true);
  drawText16(status, statusTextX, statusCenterY - 8 + statusTextOffsetY,
             COLOR_GREEN);
}

void drawFooterBase(uint8_t columns) {
  fillRect(0, FOOTER_Y, SCREEN_W, SCREEN_H - FOOTER_Y, COLOR_GREEN_PALE);
  k10.canvas->canvasSetLineWidth(1);
  k10.canvas->canvasLine(0, FOOTER_Y, SCREEN_W, FOOTER_Y, COLOR_LINE);
  for (uint8_t i = 1; i < columns; i++) {
    int16_t x = (SCREEN_W * i) / columns;
    k10.canvas->canvasLine(x, FOOTER_Y + 4, x, SCREEN_H - 4, COLOR_LINE);
  }
}

// 小图标采用 24x16 像素坐标，直接画线，不依赖 emoji 字库或 TF 卡。
void gestureIconLine(int16_t x, int16_t y, int16_t x1, int16_t y1,
                     int16_t x2, int16_t y2, uint32_t color, bool mirror) {
  if (mirror) { x1 = 23 - x1; x2 = 23 - x2; }
  k10.canvas->canvasLine(x + x1, y + y1, x + x2, y + y2, color);
}

void drawGestureIcon(uint8_t gesture, int16_t x, int16_t y, uint32_t color) {
  k10.canvas->canvasSetLineWidth(1);
  if (gesture == GESTURE_LEFT || gesture == GESTURE_RIGHT) {
    // 张开的手掌 + 移动箭头；右摆由左摆整体镜像得到。
    // 所有线条均在 24x16 图标区域内，首页与步骤页共用此图标。
    const bool mirror = (gesture == GESTURE_RIGHT);
    const int8_t points[][2] = {
      {13,15},{13,13},{10,10},{9,7},{10,6},{12,8},
      {12,3},{13,2},{14,3},{14,7},
      {14,1},{15,0},{16,1},{16,7},
      {16,2},{17,1},{18,2},{18,7},
      {18,4},{19,3},{20,4},{20,10},{18,13},{18,15},{13,15}
    };
    for (uint8_t i = 1; i < sizeof(points) / sizeof(points[0]); ++i)
      gestureIconLine(x,y,points[i-1][0],points[i-1][1],
                      points[i][0],points[i][1],color,mirror);
    // 箭头位于手掌左侧，镜像后位于右侧，明确表示摆动方向。
    gestureIconLine(x,y,0,8,7,8,color,mirror);
    gestureIconLine(x,y,0,8,3,5,color,mirror);
    gestureIconLine(x,y,0,8,3,11,color,mirror);
    // 手掌后方的短线表示移动轨迹。
    gestureIconLine(x,y,22,6,23,6,color,mirror);
    gestureIconLine(x,y,22,10,23,10,color,mirror);
  } else if (gesture == GESTURE_OK) {
    // 拇指与食指围成圆环，其余三指伸展。
    k10.canvas->canvasCircle(x+7,y+9,4,color,COLOR_GREEN_PALE,false);
    const int8_t lines[][4] = {
      {10,6,11,1},{11,1,13,1},{13,1,13,7},
      {13,7,15,0},{15,0,17,1},{17,1,16,8},
      {16,8,19,3},{19,3,21,4},{21,4,18,12},
      {18,12,15,15},{15,15,8,15},{8,15,5,12}
    };
    for (uint8_t i=0; i<sizeof(lines)/sizeof(lines[0]); ++i)
      gestureIconLine(x,y,lines[i][0],lines[i][1],lines[i][2],lines[i][3],color,false);
  } else if (gesture == GESTURE_FIST) {
    // 握拳：收拢的指节、横扣的拇指与腕口。
    const int8_t points[][2] = {
      {7,15},{7,12},{4,10},{3,7},{3,3},{5,1},{7,1},
      {8,2},{10,1},{12,1},{13,2},{15,1},{17,2},
      {19,2},{21,4},{21,9},{19,12},{17,13},{17,15},{7,15}
    };
    for (uint8_t i=1; i<sizeof(points)/sizeof(points[0]); ++i)
      gestureIconLine(x,y,points[i-1][0],points[i-1][1],points[i][0],points[i][1],color,false);
    gestureIconLine(x,y,8,2,8,6,color,false);
    gestureIconLine(x,y,13,2,13,6,color,false);
    gestureIconLine(x,y,17,2,17,6,color,false);
    gestureIconLine(x,y,20,7,10,7,color,false);
    gestureIconLine(x,y,10,7,9,9,color,false);
    gestureIconLine(x,y,9,9,17,11,color,false);
  }
}

void drawFooterItem(uint8_t gesture, const String &action,
                    uint8_t index, uint8_t columns, uint32_t color) {
  const int16_t left = SCREEN_W * index / columns;
  const int16_t right = SCREEN_W * (index + 1) / columns;
  drawGestureIcon(gesture, left + (right - left - 24) / 2, FOOTER_Y + 2, color);
  int16_t x = left + (right - left - utf8TextWidth(action, 16, 8)) / 2;
  // 保留操作说明，逐字固定字位，与图标分别居中。
  for (uint16_t i = 0; i < action.length();) {
    uint16_t end = nextCharacter(action, i);
    drawText16(action.substring(i, end), x, FOOTER_Y + 20, COLOR_MUTED);
    x += (uint8_t)action[i] < 0x80 ? 8 : 16;
    i = end;
  }
}

// -------------------- 首页 --------------------
void drawRecipeCard(uint8_t index, int16_t y) {
  const Recipe &recipe = RECIPES[index];
  bool selected = (index == selectedRecipe);
  uint32_t background = selected ? COLOR_ORANGE_PALE : COLOR_CARD;
  uint32_t border = selected ? COLOR_ORANGE : COLOR_LINE;
  const int16_t cardH = 42;
  fillRect(10, y, 300, cardH, background);
  outlineRect(10, y, 300, cardH, border);
  if (selected) fillRect(10, y, 4, cardH, COLOR_ORANGE);
  k10.canvas->canvasCircle(30, y + cardH / 2, 12,
                           selected ? COLOR_ORANGE : COLOR_GREEN,
                           selected ? COLOR_ORANGE : COLOR_GREEN, true);
  drawTextBox(String(index + 1), 18, y, 24, cardH, COLOR_WHITE, false);
  drawTextBox(recipe.name, 46, y, 182, cardH, COLOR_TEXT, true);
  drawTextBox(recipe.totalTime, 232, y + 5, 68, 16, COLOR_MUTED, false);
  drawTextBox(String(recipe.stepCount) + "步骤", 232, y + 21, 68, 16,
              COLOR_MUTED, false);
}

void ensureSelectedRecipeVisible() {
  if (selectedRecipe < recipeWindowStart) {
    recipeWindowStart = selectedRecipe;
  } else if (selectedRecipe >= recipeWindowStart + VISIBLE_RECIPE_COUNT) {
    recipeWindowStart = selectedRecipe - VISIBLE_RECIPE_COUNT + 1;
  }
  uint8_t maxStart = RECIPE_COUNT > VISIBLE_RECIPE_COUNT
                       ? RECIPE_COUNT - VISIBLE_RECIPE_COUNT : 0;
  if (recipeWindowStart > maxStart) recipeWindowStart = maxStart;
}

void drawRecipeScrollBar() {
  if (RECIPE_COUNT <= VISIBLE_RECIPE_COUNT) return;
  const int16_t trackY = 70;
  const int16_t trackH = 130;
  int16_t thumbH = (trackH * VISIBLE_RECIPE_COUNT) / RECIPE_COUNT;
  uint8_t maxStart = RECIPE_COUNT - VISIBLE_RECIPE_COUNT;
  int16_t thumbY = trackY +
    ((trackH - thumbH) * recipeWindowStart) / maxStart;
  fillRect(315, trackY, 3, trackH, COLOR_LINE);
  fillRect(314, thumbY, 5, thumbH, COLOR_ORANGE);
}

void stopStepTimer() {
  timerRunning = false;
  timerFinished = false;
  timerWaitingForSpeech = false;
}

void renderRecipeList(bool announceWelcome = false) {
  currentPage = PAGE_RECIPE_LIST;
  stopStepTimer();
  beginPage();
  drawHeader("首页");
  drawCentered24("请选择菜谱", SCREEN_W / 2, 44, COLOR_TEXT);
  ensureSelectedRecipeVisible();
  for (uint8_t row = 0; row < VISIBLE_RECIPE_COUNT; row++) {
    uint8_t recipeIndex = recipeWindowStart + row;
    if (recipeIndex < RECIPE_COUNT) {
      drawRecipeCard(recipeIndex, 70 + row * 44);
    }
  }
  drawRecipeScrollBar();
  String position = String(selectedRecipe + 1) + "/" + String(RECIPE_COUNT);
  drawText16(position, 282, 51, COLOR_MUTED);
  drawFooterBase(4);
  drawFooterItem(GESTURE_LEFT, "上一个", 0, 4, COLOR_GREEN);
  drawFooterItem(GESTURE_RIGHT, "下一个", 1, 4, COLOR_GREEN);
  drawFooterItem(GESTURE_OK, "确认选择", 2, 4, COLOR_ORANGE);
  drawFooterItem(GESTURE_FIST, "返回首页", 3, 4, COLOR_ORANGE);
  k10.canvas->updateCanvas();
  if (announceWelcome) {
    speakText("你好，欢迎使用电子菜谱助手，请选择菜谱。");
  }
}

// 方向参数复用循环选菜逻辑：-1 上一个，+1 下一个。
void selectRecipe(int8_t direction) {
  selectedRecipe = (selectedRecipe + RECIPE_COUNT + direction) % RECIPE_COUNT;
  renderRecipeList();
}

// -------------------- 食材准备页 --------------------
void renderIngredients(bool announce = true) {
  currentPage = PAGE_INGREDIENTS;
  timerRunning = false;
  timerWaitingForSpeech = false;
  const Recipe &recipe = RECIPES[selectedRecipe];
  beginPage();
  drawHeader("准备食材");
  drawTextBox(recipe.name, 10, 40, 300, 30, COLOR_ORANGE, true);
  // 右上角已经标明准备食材，中央空间用于五行大字食材。
  fillRect(10, 70, 300, 130, COLOR_CARD);
  outlineRect(10, 70, 300, 130, COLOR_LINE);
  for (uint8_t i = 0; i < 5; i++) {
    String line = String(i + 1) + ". " + recipe.ingredients[i];
    drawTextBox(line, 16, 70 + i * 26, 288, 26, COLOR_TEXT, true);
    if (i < 4) {
      k10.canvas->canvasLine(16, 96 + i * 26, 304, 96 + i * 26, COLOR_GREEN_PALE);
    }
  }
  drawFooterBase(2);
  drawFooterItem(GESTURE_OK, "食材备好后开始", 0, 2, COLOR_ORANGE);
  drawFooterItem(GESTURE_FIST, "返回菜谱首页", 1, 2, COLOR_GREEN);
  k10.canvas->updateCanvas();
  if (announce) {
    String message = "已选择" + String(recipe.name) +
                     "。请准备好所需食材，准备完成后做OK手势开始制作。";
    speakText(message);
  }
}

// -------------------- 步骤页与图片占位 --------------------
String formatTime(uint16_t seconds) {
  uint16_t minutes = seconds / 60;
  uint8_t secs = seconds % 60;
  String text = String(minutes);
  text += ":";
  if (secs < 10) text += "0";
  text += String(secs);
  return text;
}

bool currentStepNeedsTimer() {
  return RECIPES[selectedRecipe].stepSeconds[currentStep] > 0;
}

bool announceCurrentStep() {
  const Recipe &recipe = RECIPES[selectedRecipe];
  String message = "第" + String(currentStep + 1) + "步。" +
                   String(recipe.steps[currentStep]) + "。" +
                   String(recipe.stepTips[currentStep]) + "。";
  if (currentStepNeedsTimer()) {
    uint16_t seconds = recipe.stepSeconds[currentStep];
    if (seconds % 60 == 0) {
      message += "本步骤计时" + String(seconds / 60) + "分钟，计时现在开始。";
    } else {
      message += "本步骤计时" + String(seconds) + "秒，计时现在开始。";
    }
  }
  return speakText(message);
}

void resetStepTimer() {
  remainingSeconds = RECIPES[selectedRecipe].stepSeconds[currentStep];
  // 需要计时时，先等本步语音真正播放完成，再重置计时基准。
  timerRunning = remainingSeconds > 0;
  timerFinished = false;
  timerWaitingForSpeech = false;
  lastTimerTick = millis();
}

void drawStepImage() {
  fillRect(10, 48, 140, 128, COLOR_ORANGE_PALE);
  String fileName = String(RECIPE_IMAGE_PREFIXES[selectedRecipe]) +
                    String(currentStep + 1) + ".png";
  String sdPath = "/" + fileName;
  if (!SD.exists(sdPath.c_str())) {
    sdPath = "/recipe_images_preview/" + fileName;
  }
  if (SD.exists(sdPath.c_str())) {
    // SD 文件接口用 / 路径，K10 画布接口用 S:/ 路径。
    // 图片已处理为 140x128，按原始尺寸绘制，不会覆盖右侧步骤文字。
    stepImagePath = "S:" + sdPath;
    k10.canvas->canvasDrawImage(10, 48, stepImagePath);
    Serial.print(F("[Image] "));
    Serial.println(stepImagePath);
    return;
  }
  outlineRect(10, 48, 140, 128, COLOR_ORANGE);
  drawCentered16("未找到步骤图片", 80, 94, COLOR_ORANGE_DARK);
  drawCentered16("请检查TF卡文件", 80, 118, COLOR_MUTED);
  Serial.print(F("[Image] Missing file (root or recipe_images_preview): "));
  Serial.println(fileName);
}

void drawTimerPanel(bool clearFirst) {
  if (!currentStepNeedsTimer()) return;
  if (clearFirst) k10.canvas->clearLocalCanvas(164, 174, 142, 26);
  fillRect(164, 174, 142, 26, COLOR_ORANGE_PALE);
  outlineRect(164, 174, 142, 26, COLOR_ORANGE);
  uint32_t color = timerFinished ? COLOR_GREEN : COLOR_ORANGE_DARK;
  drawTextBox(timerFinished ? "完成" : "计时", 166, 174, 40, 26, color, false);
  drawTextBox(formatTime(remainingSeconds), 208, 174, 96, 26, color, true);
}

void renderCookingStep(bool announceStep = true) {
  currentPage = PAGE_COOKING_STEP;
  const Recipe &recipe = RECIPES[selectedRecipe];
  beginPage();
  drawHeader("制作中");
  drawStepImage();
  fillRect(158, 42, 152, 158, COLOR_CARD);
  outlineRect(158, 42, 152, 158, COLOR_LINE);
  String progress = "步骤 " + String(currentStep + 1) + "/" + String(recipe.stepCount);
  fillRect(164, 44, 140, 28, COLOR_ORANGE_PALE);
  drawTextBox(progress, 164, 44, 140, 28, COLOR_ORANGE_DARK, true);
  drawTextBox(recipe.steps[currentStep], 162, 74, 144, 48, COLOR_TEXT, true);
  // 主文字与辅助说明均为24像素，分别在各自区域居中。
  drawTextBox(recipe.stepTips[currentStep], 162, 124, 144,
              currentStepNeedsTimer() ? 48 : 72, COLOR_MUTED, true);
  if (currentStepNeedsTimer()) drawTimerPanel(false);
  drawFooterBase(3);
  drawFooterItem(GESTURE_LEFT, "上一步", 0, 3, COLOR_GREEN);
  drawFooterItem(GESTURE_RIGHT, "下一步", 1, 3, COLOR_GREEN);
  drawFooterItem(GESTURE_FIST, "返回首页", 2, 3, COLOR_ORANGE);
  k10.canvas->updateCanvas();
  if (announceStep) {
    bool announcementStarted = announceCurrentStep();
    if (currentStepNeedsTimer()) {
      timerWaitingForSpeech = announcementStarted;
      // 如果语音未能发送，不阻止烹饪流程，直接开始计时。
      if (!announcementStarted) lastTimerTick = millis();
    }
  }
}

// -------------------- 完成页 --------------------
void renderFinished() {
  currentPage = PAGE_FINISHED;
  stopStepTimer();
  beginPage();
  drawHeader("已完成");
  k10.canvas->canvasCircle(160, 78, 25, COLOR_GREEN, COLOR_GREEN, true);
  k10.canvas->canvasSetLineWidth(5);
  k10.canvas->canvasLine(148, 78, 157, 87, COLOR_WHITE);
  k10.canvas->canvasLine(157, 87, 174, 68, COLOR_WHITE);
  drawCentered24("制作完成", SCREEN_W / 2, 108, COLOR_TEXT);
  drawCentered24("请享用你的美食！", SCREEN_W / 2, 140, COLOR_ORANGE_DARK);
  drawFooterBase(1);
  drawFooterItem(GESTURE_FIST, "返回首页", 0, 1, COLOR_GREEN);
  k10.canvas->updateCanvas();
  speakText("制作完成，请享用。");
}

// -------------------- 页面跳转与计时控制 --------------------
void enterStep(uint8_t step) {
  currentStep = step;
  resetStepTimer();
  renderCookingStep();
}

// 统一处理步骤边界；首步后退回食材页，末步前进到完成页。
void changeStep(int8_t direction) {
  int16_t step = currentStep + direction;
  if (step < 0) renderIngredients();
  else if (step >= RECIPES[selectedRecipe].stepCount) renderFinished();
  else enterStep(step);
}

void updateTimer() {
  if (currentPage != PAGE_COOKING_STEP ||
      !currentStepNeedsTimer() || !timerRunning ||
      timerWaitingForSpeech) return;
  unsigned long now = millis();
  if (now - lastTimerTick < 1000) return;
  uint16_t elapsedSeconds = (now - lastTimerTick) / 1000;
  lastTimerTick += (unsigned long)elapsedSeconds * 1000;
  if (elapsedSeconds >= remainingSeconds) remainingSeconds = 0;
  else remainingSeconds -= elapsedSeconds;
  if (remainingSeconds == 0) {
    timerRunning = false;
    timerFinished = true;
    renderCookingStep(false);
    speakText("计时结束，本步骤已完成，请做右摆手势进入下一步。");
    return;
  }
  drawTimerPanel(true);
  k10.canvas->updateCanvas();
}

// -------------------- 手势状态机 --------------------
void handleGesture(uint8_t id) {
  Serial.print(F("Command gesture ID: "));
  Serial.println(id);
  if (id == GESTURE_FIST) {
    renderRecipeList(true);
    return;
  }
  switch (currentPage) {
    case PAGE_RECIPE_LIST:
      if (id == GESTURE_LEFT) selectRecipe(-1);
      else if (id == GESTURE_RIGHT) selectRecipe(1);
      else if (id == GESTURE_OK) renderIngredients();
      break;
    case PAGE_INGREDIENTS:
      if (id == GESTURE_OK || id == GESTURE_LEFT) enterStep(0);
      else if (id == GESTURE_RIGHT) renderRecipeList(true);
      break;
    case PAGE_COOKING_STEP:
      if (id == GESTURE_LEFT) changeStep(-1);
      else if (id == GESTURE_RIGHT) changeStep(1);
      break;
    case PAGE_FINISHED: break;
  }
}

void processGestureFrame(uint8_t id) {
  unsigned long now = millis();
  if (id == GESTURE_NONE) {
    if (now - lastGestureSeen >= 600) {
      gestureLocked = false;
      candidateGesture = GESTURE_NONE;
    }
    return;
  }
  lastGestureSeen = now;
  if (gestureLocked) return;
  if (id != candidateGesture) {
    candidateGesture = id;
    candidateSince = now;
    return;
  }
  if (now - candidateSince >= 250) {
    gestureLocked = true;
    // 稳定识别250ms后只闪一次；持续保持同一手势不会反复闪灯。
    // 即使该手势在当前页没有动作，也反馈“已识别”。
    flashGestureLed();
    handleGesture(id);
  }
}

// -------------------- 启动与主循环 --------------------
void drawBootMessage(const String &line1, const String &line2,
                     uint32_t line1Color, uint32_t line2Color) {
  beginPage();
  drawHeader("启动中");
  drawCentered24(line1, SCREEN_W / 2, 83, line1Color);
  drawCentered16(line2, SCREEN_W / 2, 123, line2Color);
  k10.canvas->updateCanvas();
}

void setup() {
  Serial.begin(115200);
  delay(500);
  k10.begin();
  digitalWrite(GESTURE_LED_PIN, GESTURE_LED_ON == HIGH ? LOW : HIGH);
  pinMode(GESTURE_LED_PIN, OUTPUT);
  k10.initScreen(SCREEN_DIR);
  k10.setScreenBackground(COLOR_BG);
  // 当前 K10 库需要先建立背景对象，Canvas 才能取得有效父对象。
  k10.initBgCamerImage();
  k10.creatCanvas();
  k10.setBgCamerImage(false);
  // 使用用户提供的 MindPlus TF 卡初始化接口，只在启动时调用一次。
  Serial.println(F("[TF] Initializing SD file system..."));
  k10.initSDFile();
  Serial.println(F("[TF] SD initialization returned."));
  // 语音模块与手势传感器共用 K10 的 I2C 总线，但地址互不冲突。
  Wire.begin();
  drawBootMessage("正在连接语音模块", "V1 · I2C 0x40",
                  COLOR_ORANGE, COLOR_MUTED);
  delay(800);
  // 按已在 K10 实机验证通过的 V1 流程直接初始化。
  // DFRobot V1 库内部会等待模块应答，所以保留分段串口日志方便定位。
  Wire.setClock(SPEECH_I2C_CLOCK);
  Serial.println(F("[Speech] begin eV1..."));
  speech.begin(speech.eV1);
  Serial.println(F("[Speech] begin eV1 OK."));
  Wire.setClock(SPEECH_I2C_CLOCK);
  Serial.println(F("[Speech] set volume..."));
  speech.setVolume(8);
  Serial.println(F("[Speech] set volume OK."));
  Serial.println(F("[Speech] set speed..."));
  speech.setSpeed(5);
  Serial.println(F("[Speech] set speed OK."));
  Serial.println(F("[Speech] set tone..."));
  speech.setTone(5);
  Serial.println(F("[Speech] set tone OK."));
  Serial.println(F("[Speech] set male voice..."));
  speech.setSoundType(speech.eMale1);
  Serial.println(F("[Speech] set male voice OK."));
  speechReady = true;
  Serial.println(F("V1 speech module initialized at I2C 0x40."));
  restoreGestureI2CClock();
  drawBootMessage("正在连接手势传感器", "SEN0670 · I2C 0x3A · 400 kHz",
                  COLOR_ORANGE, COLOR_MUTED);
  delay(1500);
  uint32_t attempts = 0;
  while (!sensor.begin()) {
    attempts++;
    drawBootMessage("传感器初始化失败", "正在自动重试：" + String(attempts),
                    COLOR_RED, COLOR_ORANGE);
    Serial.println(F("SEN0670 initialization failed; retrying..."));
    delay(1000);
  }
  delay(2000);
  uint32_t modelAttempts = 0;
  DFRobot_HumanPose::eCmdCode_t modelResult = DFRobot_HumanPose::eTimedOut;
  while (modelResult != DFRobot_HumanPose::eOK) {
    modelAttempts++;
    modelResult = sensor.setModelType(DFRobot_HumanPose::eHand);
    if (modelResult == DFRobot_HumanPose::eOK) break;
    String detail = "返回码 " + String((int)modelResult) +
                    " · 重试 " + String(modelAttempts);
    drawBootMessage("正在设置自学习模型", detail,
                    COLOR_ORANGE, COLOR_MUTED);
    Serial.print(F("Gesture model setup failed, code="));
    Serial.print((int)modelResult);
    Serial.print(F(", retry="));
    Serial.println(modelAttempts);
    delay(1500);
  }
  Serial.print(F("Gesture model selected after attempts: "));
  Serial.println(modelAttempts);
  Serial.println(F("Hybrid: eHand 1=LEFT 2=RIGHT; eGesture 8=OK 0=FIST."));
  Serial.println(F("Landscape electronic recipe assistant is ready."));
  renderRecipeList(true);
}

// 两个模型轮换；切换后留出图像推理稳定时间，再开始读取。
// 这些时间是初始调试值，需按实机响应调整。
const uint32_t MODEL_SETTLE_MS = 200;
const uint32_t MODEL_SAMPLE_MS = 650;
const uint32_t MODEL_RETRY_MS = 1000;
bool readingLearnedModel = true; // setup 最后选择的是 eHand
bool modelSwitchFailed = false;
unsigned long modelReadyAt = 0;
unsigned long modelWindowSince = 0;
unsigned long modelRetryAt = 0;

uint8_t mapGestureResult(uint8_t rawId) {
  if (readingLearnedModel) {
    if (rawId == 1) return GESTURE_LEFT;
    if (rawId == 2) return GESTURE_RIGHT;
  } else {
    if (rawId == 8) return GESTURE_OK;
    if (rawId == 0) return GESTURE_FIST;
  }
  return GESTURE_NONE;
}

void switchGestureModel() {
  const bool nextLearned = !readingLearnedModel;
  restoreGestureI2CClock();
  const DFRobot_HumanPose::eCmdCode_t status = sensor.setModelType(
    nextLearned ? DFRobot_HumanPose::eHand : DFRobot_HumanPose::eGesture);
  if (status != DFRobot_HumanPose::eOK) {
    // 返回失败时无法确定设备端模型，不读取结果，等待重试同一目标。
    modelSwitchFailed = true;
    modelRetryAt = millis() + MODEL_RETRY_MS;
    Serial.println(F("[Gesture] model switch failed; retrying."));
    return;
  }
  readingLearnedModel = nextLearned;
  modelSwitchFailed = false;
  while (sensor.availableResult()) sensor.popResult();
  candidateGesture = GESTURE_NONE;
  candidateSince = millis();
  modelReadyAt = millis() + MODEL_SETTLE_MS;
  modelWindowSince = modelReadyAt;
}

void updateHybridGestures() {
  unsigned long now = millis();
  if (modelSwitchFailed) {
    if ((long)(now - modelRetryAt) >= 0) switchGestureModel();
    return;
  }
  if ((long)(now - modelReadyAt) < 0) return;
  restoreGestureI2CClock();
  if (sensor.getResult() != DFRobot_HumanPose::eOK) {
    // 通信失败不是手移开，不用失败帧解锁或累积稳定识别时间。
    candidateGesture = GESTURE_NONE;
    if (!gestureLocked && millis() - modelWindowSince >= MODEL_SAMPLE_MS)
      switchGestureModel();
    return;
  }
  uint8_t bestGesture = GESTURE_NONE;
  uint8_t bestScore = 0;
  bool anyHandPresent = false;
  while (sensor.availableResult()) {
    Result *result = sensor.popResult();
    if (result == NULL) continue;
    // eHand 的 ID0 仍表示检测到未学习的手；固定模型 ID13 表示无手势。
    if (readingLearnedModel || result->id != 13) anyHandPresent = true;
    const uint8_t mapped = mapGestureResult(result->id);
    if (mapped != GESTURE_NONE && result->score >= bestScore) {
      bestGesture = mapped;
      bestScore = result->score;
    }
  }
  if (gestureLocked && anyHandPresent) lastGestureSeen = millis();
  if (!gestureLocked && bestGesture == GESTURE_NONE)
    candidateGesture = GESTURE_NONE;
  processGestureFrame(bestGesture);
  // 触发后停留在当前模型，直到手移开 600ms；保持手势不会跨模型再触发。
  if (!gestureLocked && millis() - modelWindowSince >= MODEL_SAMPLE_MS)
    switchGestureModel();
}

void loop() {
  updateGestureLed();
  updateSpeech();
  updateHybridGestures();
  updateTimer();
  updateGestureLed();
  delay(50);
}
