/* Семантическая память - основа моделей понимания.
Связь id стимула (образ) с его значимостью в данных условиях, что порождает абстракцию.
Каждый акт формирования семантической памяти требует слепка текущих условий (контекста):
id базового состояния WellNormaBad (1 / 2 / 3) и сочетания ID активных базовых контекстов (EmotionId).
Значимость берется из значимости текущего состояния z = getDiffImportance()
по сравнению с состоянием до контакта с объектом.

«Лупа» значимости (registerSemanticSample): если по данной тройке (well, emotion, finalImage) уже накопленная
средняя значимость meanImportance по модулю не превышает 0,5, и ни один витал не вышел из нормы (Kbad = 0),
в память записывается не сам z, а normalizeValue(clamp(raw × 5, ±3)), где raw — неокруглённый diff из
getDiffImportance (LastDiffImportanceRaw в prepare.js), т.е. усиление слабого сигнала при «тихой» семантике
и спокойном теле (без искажения при стрессе по виталам).

При взаимодействии с объектом:
1) До запуска внутренних рефлексов вызывается getDiffImportance() — фиксация "нуля".
2) После срабатывания рефлексов (через 1 пульс, когда оформилось воздействие) снова вызывается getDiffImportance().
   Полученное z — "семантическая значимость" данного объекта (FinalImageId) в текущих условиях
   (WellNormaBad + EmotionId).
3) Для комбинации (WellNormaBad, EmotionId, FinalImageId) создаётся / обновляется объект SemanticObjectImportance
   в массиве SemanticMemory.
*/

/* Класс семантической значимости объекта при взаимодействии с ним (в том числе пассивном).
Уникальность записи определяют:
- wellNormaBad (1, 2, 3) — базовое состояние "Плохо / Норма / Хорошо" на момент взаимодействия;
- emotionId — ID сочетания базовых контекстов (Emotion);
- finalImageId — ID сочетания активных стимулов (FinalImage).

Каждый новый опыт с таким же (wellNormaBad, emotionId, finalImageId) модифицирует итоговую значимость:
по текущему z и samplesCount пересчитывается новое среднее meanImportance.

ВАЖНО ПОНИМАТЬ, что если действие не изменяет витал (он уже в 0 или в 100) то и эффекта от действия не будет. 
Т.е. Поощрить даст эффект если Стресс имеет некоторое значение, которое уменьшится.
*/

(function (global) {
  'use strict';

  // Класс одной записи семантической памяти
  function SemanticObjectImportance(wellNormaBad, emotionId, finalImageId, initialImportance) {
    this.wellNormaBad = wellNormaBad;      // 1 / 2 / 3
    this.emotionId = emotionId;            // ID Emotion
    this.finalImageId = finalImageId;      // ID FinalImage

    this.meanImportance = initialImportance || 0; // среднее значение z
    this.samplesCount = 0;                        // сколько раз наблюдали
    this.lastImportance = 0;                      // последнее z

    if (typeof initialImportance === 'number') {
      this.meanImportance = initialImportance;
      this.lastImportance = initialImportance;
      this.samplesCount = 1;
    }
  }

  SemanticObjectImportance.prototype.addSample = function (z) {
    if (typeof z !== 'number' || isNaN(z)) {
      return;
    }
    var n = this.samplesCount;
    if (n <= 0) {
      this.meanImportance = z;
      this.samplesCount = 1;
      this.lastImportance = z;
      return;
    }
    // Обновляем среднее: новое = (старое*n + z) / (n+1)
    this.meanImportance = (this.meanImportance * n + z) / (n + 1);
    this.samplesCount = n + 1;
    this.lastImportance = z;
  };

  // Глобальный массив объектов семантической памяти
  if (!Array.isArray(global.SemanticMemory)) {
    global.SemanticMemory = [];
  }

  // Известные FinalImageId и EmotionId можно хранить как простые массивы,
  // чтобы их тоже сохранять/восстанавливать.
  if (!Array.isArray(global.KnownFinalImageIds)) {
    global.KnownFinalImageIds = [];
  }
  if (!Array.isArray(global.KnownEmotionIds)) {
    global.KnownEmotionIds = [];
  }

  // Индекс для быстрого поиска по (wellNormaBad, emotionId, finalImageId)
  // Структура: SemanticMemoryIndex[wellNormaBad][emotionId][finalImageId] = SemanticObjectImportance
  if (!global.SemanticMemoryIndex) {
    global.SemanticMemoryIndex = {};
  }

  function getIndexBucket(wellNormaBad, emotionId) {
    var wKey = String(wellNormaBad);
    var eKey = String(emotionId);
    if (!global.SemanticMemoryIndex[wKey]) {
      global.SemanticMemoryIndex[wKey] = {};
    }
    if (!global.SemanticMemoryIndex[wKey][eKey]) {
      global.SemanticMemoryIndex[wKey][eKey] = {};
    }
    return global.SemanticMemoryIndex[wKey][eKey];
  }

  /**
   * Быстрый поиск объекта семантической значимости по индексу O(1).
   * @param {number} well — базовое состояние (1/2/3).
   * @param {number} emotionId — ID эмоции.
   * @param {number} finalImageId — ID образа.
   * @returns {SemanticObjectImportance|null}
   */
  function getSemanticObjectFast(well, emotionId, finalImageId) {
    if (!well || !emotionId || !finalImageId || !global.SemanticMemoryIndex) return null;
    var bucket = global.SemanticMemoryIndex[String(well)];
    if (!bucket) return null;
    bucket = bucket[String(emotionId)];
    if (!bucket) return null;
    return bucket[String(finalImageId)] || null;
  }

  /**
   * Все кадры семантической памяти для одного finalImageId (обход по индексу, без линейного скана SemanticMemory).
   * @param {number} finalImageId
   * @returns {Object[]} массив SemanticObjectImportance.
   */
  /** true, если хотя бы один витал с Kbad > 0 (как в prepare.calculateBadVitalsValues). */
  function semanticMemory_anyVitalOutOfNorm() {
    var bad = global.BadVitalsValue;
    var arr = global.VitalsArr;
    if (!bad || !arr || !arr.length) return false;
    for (var vi = 0; vi < arr.length; vi++) {
      var vid = arr[vi].Id;
      var kb = bad[vid];
      if (kb == null) kb = bad[String(vid)];
      if (typeof kb === 'number' && kb > 0) return true;
    }
    return false;
  }

  function getSemanticFramesForFinalImageId(finalImageId) {
    var result = [];
    if (!global.SemanticMemoryIndex || finalImageId == null) return result;
    var fKey = String(finalImageId);
    var wKeys = Object.keys(global.SemanticMemoryIndex);
    for (var w = 0; w < wKeys.length; w++) {
      var byEmotion = global.SemanticMemoryIndex[wKeys[w]];
      if (!byEmotion) continue;
      var eKeys = Object.keys(byEmotion);
      for (var e = 0; e < eKeys.length; e++) {
        var obj = byEmotion[eKeys[e]] && byEmotion[eKeys[e]][fKey];
        if (obj) result.push(obj);
      }
    }
    return result;
  }

  function getOrCreateSemanticObject(wellNormaBad, emotionId, finalImageId, initialImportance) {
    if (!wellNormaBad || !emotionId || !finalImageId) {
      return null;
    }
    var bucket = getIndexBucket(wellNormaBad, emotionId);
    var fKey = String(finalImageId);
    var obj = bucket[fKey];
    if (!obj) {
      obj = new SemanticObjectImportance(wellNormaBad, emotionId, finalImageId, initialImportance);
      bucket[fKey] = obj;
      global.SemanticMemory.push(obj);

      if (global.KnownFinalImageIds.indexOf(finalImageId) === -1) {
        global.KnownFinalImageIds.push(finalImageId);
      }
      if (global.KnownEmotionIds.indexOf(emotionId) === -1) {
        global.KnownEmotionIds.push(emotionId);
      }
    } else if (typeof initialImportance === 'number') {
      obj.addSample(initialImportance);
    }
    return obj;
  }

  /**
   * Регистрация одного семантического опыта.
   * Вызывается ПОСЛЕ второго getDiffImportance(), когда есть значение z.
   *
   * «Лупа» значимости: при |meanImportance| ≤ 0,5 для этой тройки и отсутствии выхода виталов из нормы
   * в выборку идёт normalizeValue(clamp(LastDiffImportanceRaw × 5, ±3)) вместо z — см. комментарий в шапке файла.
   * Требуется, что непосредственно перед вызовом был getDiffImportance() (заполняется LastDiffImportanceRaw).
   *
   * @param {number} finalImageId - ID текущего FinalImage (сочетание активных стимулов).
   * @param {number} emotionId - ID текущего Emotion (сочетание базовых контекстов).
   * @param {number} z - значимость объекта (результат getDiffImportance(); при лупе заменяется усиленным значением).
   */
  function registerSemanticSample(finalImageId, emotionId, z) {
    if (typeof z !== 'number' || isNaN(z)) {
      return;
    }
    var well = global.BadNormWell || 0; // 1 / 2 / 3
    if (!well) {
      return;
    }

    var LOUPE_PRIOR_MEAN_ABS = 0.5;
    var LOUPE_RAW_SCALE = 5;
    var LOUPE_RAW_CLAMP = 3;

    var priorObj = getSemanticObjectFast(well, emotionId, finalImageId);
    var priorMean = priorObj && typeof priorObj.meanImportance === 'number' && !isNaN(priorObj.meanImportance)
      ? priorObj.meanImportance
      : 0;
    var zApplied = z;
    if (Math.abs(priorMean) <= LOUPE_PRIOR_MEAN_ABS && !semanticMemory_anyVitalOutOfNorm()) {
      var raw = global.LastDiffImportanceRaw;
      if (typeof raw === 'number' && !isNaN(raw)) {
        var mag = raw * LOUPE_RAW_SCALE;
        if (mag > LOUPE_RAW_CLAMP) mag = LOUPE_RAW_CLAMP;
        if (mag < -LOUPE_RAW_CLAMP) mag = -LOUPE_RAW_CLAMP;
        if (typeof global.normalizeValue === 'function') {
          zApplied = global.normalizeValue(mag);
        } else {
          zApplied = mag;
        }
      }
    }

    // Один вызов = один образец: getOrCreateSemanticObject при существующем объекте уже вызывает obj.addSample(z).
    // Дополнительный addSample здесь приводил к +2 за активацию (счётчик 0→1 верно, далее +2).
    var obj = getOrCreateSemanticObject(well, emotionId, finalImageId, zApplied);
    if (!obj) return;

    // Точечно обновляем индикатор готовности ОР для кнопок,
    // относящихся к этому finalImageId.
    // UI обновляется точечно после изменения семантики, чтобы не делать перерасчёт
    // для всех кнопок на каждом пульсе.
    if (typeof global.updateStimulusButtonsReadinessViewForFinalImageId === 'function') {
      global.updateStimulusButtonsReadinessViewForFinalImageId(finalImageId);
    }
    // Сохраняем семантическую память сразу по факту изменения (после определения последствий действий),
    // а не по каждому пульсу. Используем подпись состояния, чтобы не писать одинаковое.
    var state = serializeSemanticState();
    var sig;
    try {
      sig = JSON.stringify(state);
    } catch (e) {
      sig = '';
    }
    if (sig && sig === lastSemanticSavedSig) return;
    lastSemanticSavedSig = sig;
    saveSemanticStateToIndexedDB(state);
  }

  // ---- Сериализация / десериализация состояния для IndexedDB и файлов ----

  function serializeSemanticState() {
    var plain = [];
    for (var i = 0; i < global.SemanticMemory.length; i++) {
      var it = global.SemanticMemory[i];
      plain.push({
        wellNormaBad: it.wellNormaBad,
        emotionId: it.emotionId,
        finalImageId: it.finalImageId,
        meanImportance: it.meanImportance,
        samplesCount: it.samplesCount,
        lastImportance: it.lastImportance
      });
    }
    return {
      semanticMemory: plain,
      knownFinalImageIds: global.KnownFinalImageIds.slice(0),
      knownEmotionIds: global.KnownEmotionIds.slice(0),
      // Дополнительно сохраняем структуру FinalImages и Emotions,
      // чтобы после перезапуска восстановить соответствия finalImageId и emotionId.
      finalImages: Array.isArray(global.FinalImages) ? global.FinalImages.slice(0) : [],
      emotions: Array.isArray(global.Emotions) ? global.Emotions.slice(0) : []
    };
  }

  function clearSemanticStructures() {
    global.SemanticMemory = [];
    global.SemanticMemoryIndex = {};
    global.KnownFinalImageIds = [];
    global.KnownEmotionIds = [];
  }

  /**
   * Сбросить всю семантическую память (вызов по кнопке «Sx» в UI).
   * Очищает массивы и сохраняет пустое состояние в IndexedDB.
   */
  function clearAllSemanticMemory() {
    clearSemanticStructures();
    var state = serializeSemanticState();
    lastSemanticSavedSig = JSON.stringify(state);
    saveSemanticStateToIndexedDB(state);
  }

  /**
   * Восстановить поля объекта из сохранённой записи без логики addSample (иначе при повторном
   * совпадении ключа в массиве или при передаче meanImportance в getOrCreate искажался бы samplesCount).
   */
  function restoreSemanticObjectFromPlain(p) {
    if (!p) return;
    var w = parseInt(p.wellNormaBad, 10);
    var e = parseInt(p.emotionId, 10);
    var f = parseInt(p.finalImageId, 10);
    if (!w || !e || !f) return;
    var obj = getOrCreateSemanticObject(w, e, f, undefined);
    if (!obj) return;
    if (p.meanImportance != null && typeof p.meanImportance === 'number' && !isNaN(p.meanImportance)) {
      obj.meanImportance = p.meanImportance;
    } else if (p.meanImportance != null && !isNaN(Number(p.meanImportance))) {
      obj.meanImportance = Number(p.meanImportance);
    }
    if (p.lastImportance != null && typeof p.lastImportance === 'number' && !isNaN(p.lastImportance)) {
      obj.lastImportance = p.lastImportance;
    } else if (p.lastImportance != null && !isNaN(Number(p.lastImportance))) {
      obj.lastImportance = Number(p.lastImportance);
    }
    var sc = parseInt(p.samplesCount, 10);
    if (!isNaN(sc) && sc >= 0) {
      obj.samplesCount = sc;
    } else if (typeof p.samplesCount === 'number' && !isNaN(p.samplesCount) && p.samplesCount >= 0) {
      obj.samplesCount = p.samplesCount;
    } else {
      if (!obj.samplesCount && obj.meanImportance) {
        obj.samplesCount = 1;
      }
    }
  }

  function applySemanticState(state) {
    clearSemanticStructures();
    if (!state) {
      return;
    }
    if (Array.isArray(state.knownFinalImageIds)) {
      global.KnownFinalImageIds = state.knownFinalImageIds.slice(0);
    }
    if (Array.isArray(state.knownEmotionIds)) {
      global.KnownEmotionIds = state.knownEmotionIds.slice(0);
    }
    if (Array.isArray(state.finalImages)) {
      global.FinalImages = state.finalImages.slice(0);
    }
    if (Array.isArray(state.emotions)) {
      global.Emotions = state.emotions.slice(0);
    }

    if (Array.isArray(state.semanticMemory)) {
      for (var i = 0; i < state.semanticMemory.length; i++) {
        restoreSemanticObjectFromPlain(state.semanticMemory[i]);
      }
    }
  }

  // ---- IndexedDB: постоянное хранение ----

  var semanticDb = null;
  // Версия 4: добавлено хранилище preferences (последняя папка для Сохранить/Загрузить).
  var SEMANTIC_DB_NAME = 'BEAST_DB';
  var SEMANTIC_DB_VERSION = 4;
  var SEMANTIC_STORE = 'semantic_state';
  var SEMANTIC_KEY = 'semantic_state_singleton';
  var MEMORY_SNAPSHOT_STORE = 'memory_snapshot';
  var MEMORY_SNAPSHOT_KEY = 'memory_singleton';
  var PREFERENCES_STORE = 'preferences';
  var LAST_MEMORY_DIR_KEY = 'lastMemoryDirectory';
  var lastSemanticSavedSig = '';

  function openSemanticDb() {
    if (!global.indexedDB || !global.IndexedDBModule) {
      return Promise.reject(new Error('IndexedDB не поддерживается или модуль не загружен.'));
    }
    return global.IndexedDBModule.openDatabase({
      dbName: SEMANTIC_DB_NAME,
      version: SEMANTIC_DB_VERSION,
      stores: [
        { name: SEMANTIC_STORE, options: { keyPath: 'id' } },
        { name: MEMORY_SNAPSHOT_STORE, options: { keyPath: 'id' } },
        { name: PREFERENCES_STORE, options: { keyPath: 'id' } }
      ]
    }).then(function (db) {
      semanticDb = db;
      return db;
    });
  }

  function loadSemanticStateFromIndexedDB() {
    if (!global.IndexedDBModule) {
      return;
    }
    openSemanticDb()
      .then(function (db) {
        return global.IndexedDBModule.get({
          db: db,
          storeName: SEMANTIC_STORE,
          key: SEMANTIC_KEY
        });
      })
      .then(function (record) {
        if (record && record.state) {
          applySemanticState(record.state);
          lastSemanticSavedSig = JSON.stringify(serializeSemanticState());

          // После восстановления семантики обновляем индикаторы на кнопках один раз,
          // чтобы "готовые" бордеры соответствовали восстановленному состоянию.
          if (typeof global.updateStimulusButtonsReadinessView === 'function') {
            global.updateStimulusButtonsReadinessView();
            global.setTimeout(function () {
              if (typeof global.updateStimulusButtonsReadinessView === 'function') {
                global.updateStimulusButtonsReadinessView();
              }
            }, 0);
          }
        }
      })
      .catch(function () {
        // тихо игнорируем ошибки загрузки
      });
  }

  function saveSemanticStateToIndexedDB(state) {
    if (!global.IndexedDBModule) {
      return;
    }
    state = state || serializeSemanticState();
    function doPut(db) {
      global.IndexedDBModule.put({
        db: db,
        storeName: SEMANTIC_STORE,
        value: {
          id: SEMANTIC_KEY,
          state: state,
          ts: Date.now()
        }
      }).catch(function () {
        // ошибки записи игнорируем
      });
    }
    if (semanticDb) {
      doPut(semanticDb);
      return;
    }
    openSemanticDb().then(function (db) {
      semanticDb = db;
      doPut(db);
    }).catch(function () {
      // не удалось открыть БД — сохранить не получится
    });
  }

  /**
   * Сохранить последнюю выбранную папку для диалогов Сохранить/Загрузить память.
   * Handle хранится в IndexedDB (FileSystemHandle поддерживается в Chrome).
   * @param {FileSystemDirectoryHandle} dirHandle
   * @returns {Promise<void>}
   */
  function saveLastMemoryDirectory(dirHandle) {
    if (!dirHandle || !global.IndexedDBModule) return Promise.resolve();
    function doPut(db) {
      return global.IndexedDBModule.put({
        db: db,
        storeName: PREFERENCES_STORE,
        value: { id: LAST_MEMORY_DIR_KEY, handle: dirHandle }
      });
    }
    if (semanticDb) return doPut(semanticDb);
    return openSemanticDb().then(doPut).catch(function () {});
  }

  /**
   * Получить последнюю сохранённую папку для startIn в диалоге выбора файла.
   * @returns {Promise<FileSystemDirectoryHandle|undefined>}
   */
  function loadLastMemoryDirectory() {
    if (!global.IndexedDBModule) return Promise.resolve(undefined);
    function doGet(db) {
      return global.IndexedDBModule.get({
        db: db,
        storeName: PREFERENCES_STORE,
        key: LAST_MEMORY_DIR_KEY
      }).then(function (record) {
        return record && record.handle ? record.handle : undefined;
      });
    }
    if (semanticDb) return doGet(semanticDb);
    return openSemanticDb().then(doGet).catch(function () { return undefined; });
  }

  // Инициализация при загрузке модуля
  loadSemanticStateFromIndexedDB();

  // Экспорт в глобальное пространство имён
  global.SemanticObjectImportance = SemanticObjectImportance;
  global.getOrCreateSemanticObject = getOrCreateSemanticObject;
  global.registerSemanticSample = registerSemanticSample;
  global.serializeSemanticState = serializeSemanticState;
  global.applySemanticState = applySemanticState;
  global.saveLastMemoryDirectory = saveLastMemoryDirectory;
  global.loadLastMemoryDirectory = loadLastMemoryDirectory;
  global.clearAllSemanticMemory = clearAllSemanticMemory;
  global.getSemanticObjectFast = getSemanticObjectFast;
  global.getSemanticFramesForFinalImageId = getSemanticFramesForFinalImageId;

  function bindResetSemanticButton() {
    try {
      var btn = global.document && global.document.getElementById('btnResetSemanticMemory');
      if (btn && typeof btn.addEventListener === 'function') {
        btn.addEventListener('click', function () {
          var msg = 'Точно сбросить всю семантическую память?';
          if (typeof global.show_confirm === 'function') {
            global.show_confirm(msg, function (result) {
              if (result) clearAllSemanticMemory();
            });
          } else if (global.confirm(msg)) {
            clearAllSemanticMemory();
          }
        });
      }
    } catch (e) {}
  }
  if (global.document && global.document.readyState === 'loading') {
    global.document.addEventListener('DOMContentLoaded', bindResetSemanticButton);
  } else {
    bindResetSemanticButton();
  }

})(window);
