/**
 * Гештальты (доминанты нерешённых задач) и буфер резонанса для уровня 4.
 *
 * Полная теория: beast/_9_Conscience/about_gestalt.md
 *
 * Поток данных (кратко):
 *   1) Ур.3 в начале тика вызывает Gestalt_prepareContextForCycle — в global.GestaltContextForLevel3
 *      кладётся снимок открытого гештальта по цели главного цикла (если есть).
 *   2) Ур.4 вызывает Gestalt_onConsciousnessTick — привязка «хозяина», разбор GestaltResonanceBuffer,
 *      повторное обновление контекста после возможных инсайтов по аналогии.
 *   3) EpisodicMemory_closeLastFrame → Gestalt_onEpisodeFeedbackClosed — в буфер кладётся сигнатура
 *      закрытого кадра; при наличии goalImageId у цикла ожидания обновляется запись гештальта.
 *
 * Логи цикла: Consciousness_cycleAppendLog(cycle, text, puls [, { force: true }]).
 * Сигнатура диспетчера: см. consciousness_dispatcher.js — для цикла expired / не главного нужен force,
 * иначе cycleAppendLog отбросит запись.
 */
(function (global) {
  'use strict';

  /** Статусы гештальта: 0 новый → 1 начато → 2 частично → 3 закрыт успехом → 4 архив. */
  var GESTALT_STATUS = {
    NEW: 0,
    STARTED: 1,
    PARTIAL: 2,
    CLOSED_SUCCESS: 3,
    ARCHIVED: 4
  };

  /** Метки типа инсайта для поля lastInsightKind и строк лога. */
  var INSIGHT_KIND = {
    PRE: 'pre',
    LOCAL: 'local',
    ANALOGY: 'analogy',
    FEEDBACK: 'feedback'
  };

  /** Макс. записей в буфере резонанса (FIFO, старые выталкиваются). */
  var MAX_RESONANCE_BUFFER = 48;
  /** Макс. элементов в analogyPool у одного гештальта. */
  var MAX_ANALOGY_POOL = 16;
  /**
   * Порог «сильного» эффекта для закрытия гештальта (переход в CLOSED_SUCCESS).
   * Диапазон effect в ЭП: [-10, 10] — см. episodic_memory.closeLastFrame.
   */
  var EFFECT_STRONG_CLOSE = 6;
  /** Минимальный score для инсайта по резонансу (эвристика v1, см. scoreResonanceAgainstGestalt). */
  var RESONANCE_SCORE_INSIGHT = 0.55;

  var gestaltDirty = false;
  var lastSavedSig = '';

  if (!Array.isArray(global.GestaltArr)) global.GestaltArr = [];
  if (typeof global.GestaltNextId !== 'number' || global.GestaltNextId < 1) global.GestaltNextId = 1;
  if (!Array.isArray(global.GestaltResonanceBuffer)) global.GestaltResonanceBuffer = [];
  if (typeof global.GestaltResonanceSeq !== 'number' || global.GestaltResonanceSeq < 1) global.GestaltResonanceSeq = 1;
  if (typeof global.Gestalt_activeMasterCycleId !== 'number') global.Gestalt_activeMasterCycleId = 0;
  if (typeof global.Gestalt_activeGestaltId !== 'number') global.Gestalt_activeGestaltId = 0;

  global.GESTALT_STATUS = GESTALT_STATUS;
  global.GESTALT_INSIGHT_KIND = INSIGHT_KIND;

  function puls() {
    return typeof global.cpuls === 'number' ? global.cpuls : 0;
  }

  function markDirty() {
    gestaltDirty = true;
  }

  /**
   * Приводит объект гештальта к предсказуемой форме при создании и после загрузки из IndexedDB.
   * Копируем массивы, чтобы сериализация не делила ссылку с живым состоянием по ошибке.
   */
  function normalizeGestalt(g) {
    return {
      id: g.id,
      problemTreeId: g.problemTreeId || 0,
      situationId: g.situationId || 0,
      goalImageId: g.goalImageId || 0,
      automatismIds: Array.isArray(g.automatismIds) ? g.automatismIds.slice() : [],
      status: typeof g.status === 'number' ? g.status : 0,
      lastInsightPuls: typeof g.lastInsightPuls === 'number' ? g.lastInsightPuls : 0,
      lastInsightKind: typeof g.lastInsightKind === 'string' ? g.lastInsightKind : '',
      insightChainCount: typeof g.insightChainCount === 'number' ? g.insightChainCount : 0,
      analogyPool: Array.isArray(g.analogyPool) ? g.analogyPool.slice() : []
    };
  }

  /** Не более одной записи на goalImageId в GestaltArr (инвариант поддерживается при создании). */
  function findGestaltByGoal(goalImageId) {
    var gid = parseInt(goalImageId, 10) || 0;
    if (!gid) return null;
    for (var i = 0; i < global.GestaltArr.length; i++) {
      var g = global.GestaltArr[i];
      if (g && g.goalImageId === gid) return g;
    }
    return null;
  }

  /**
   * Открытый гештальт: status 0, 1 или 2.
   * TODO (сценарии): при «повторной» нерешённой задаче для той же цели после status 3 — либо новая запись
   * с новым id (если разрешить несколько строк на цель в архиве), либо reopen этой же записи (сброс status).
   * Сейчас: одна строка на goalImageId; после 3 обновления идут только через фидбек (редкое переоткрытие — вручную/API).
   */
  function isGestaltOpen(g) {
    return g && g.status !== GESTALT_STATUS.CLOSED_SUCCESS && g.status !== GESTALT_STATUS.ARCHIVED;
  }

  /** Добавляет ID автоматизма в список опробованных, без дубликатов. */
  function appendAutomatismIfNew(g, aid) {
    aid = parseInt(aid, 10) || 0;
    if (!aid) return false;
    if (!Array.isArray(g.automatismIds)) g.automatismIds = [];
    for (var i = 0; i < g.automatismIds.length; i++) {
      if (g.automatismIds[i] === aid) return false;
    }
    g.automatismIds.push(aid);
    return true;
  }

  /**
   * Обёртка над Consciousness_cycleAppendLog: третий аргумент — номер пульса (как в диспетчере).
   * Без force пишет только для главного цикла; force: true — для фонового и после expired (как ур.3).
   */
  function logToCycle(cycle, text, force) {
    if (!cycle || typeof global.Consciousness_cycleAppendLog !== 'function') return;
    if (!force && !cycle.isMainCycle) return;
    global.Consciousness_cycleAppendLog(cycle, text, puls(), force ? { force: true } : undefined);
  }

  /** Если цикл не передан — пробуем главный (события из ЭП без привязки к шагу ФО). */
  function logToCycleOrMain(cycle, text, force) {
    var c = cycle;
    if (!c && typeof global.Consciousness_getMainCycle === 'function') {
      c = global.Consciousness_getMainCycle();
    }
    logToCycle(c, text, force);
  }

  /**
   * Фиксирует инсайт в полях гештальта и пишет строку в лог цикла (если cycleForLog задан).
   * Всегда с force: true — инсайт важен, цикл ожидания часто expired / не главный.
   */
  function registerInsight(g, kind, detail, cycleForLog) {
    g.lastInsightPuls = puls();
    g.lastInsightKind = kind;
    g.insightChainCount = (g.insightChainCount || 0) + 1;
    var msg = 'Гештальт #' + g.id + ' (цель ' + g.goalImageId + '): инсайт [' + String(kind) + '] ' + String(detail || '');
    if (cycleForLog) logToCycle(cycleForLog, msg, true);
    markDirty();
    if (typeof global.info_insight === 'function') {
      global.info_insight(cycleForLog, { insightKind: kind, detail: detail });
    }
  }

  /**
   * Создать запись, если для goalImageId ещё нет строки.
   * @param {Object} [hint] — problemTreeId, situationId (подсказка из ветки/инфокартины)
   */
  function Gestalt_createOrGetForGoal(goalImageId, hint) {
    var gid = parseInt(goalImageId, 10) || 0;
    if (!gid) return null;
    var ex = findGestaltByGoal(gid);
    if (ex) return ex;
    hint = hint || {};
    var id = global.GestaltNextId++;
    var rec = normalizeGestalt({
      id: id,
      problemTreeId: hint.problemTreeId || 0,
      situationId: hint.situationId || 0,
      goalImageId: gid,
      automatismIds: [],
      status: GESTALT_STATUS.NEW,
      lastInsightPuls: 0,
      lastInsightKind: '',
      insightChainCount: 0,
      analogyPool: []
    });
    global.GestaltArr.push(rec);
    markDirty();
    logToCycleOrMain(null, 'Гештальт: создана запись #' + rec.id + ' для образа цели goalImageId=' + gid +
      (hint.problemTreeId ? (', ветка ' + hint.problemTreeId) : '') +
      (hint.situationId ? (', ситуация ' + hint.situationId) : '') + '.', false);
    return rec;
  }

  /**
   * Буфер резонанса: каждая запись — сигнатура закрытого кадра ЭП для кросс-цикловой аналогии.
   * @returns {Object} созданная сигнатура (для лога в вызывающем коде).
   *
   * TODO (расширение): goal_vector / precondition_mask из семантики — см. about_gestalt.md §6.
   */
  function Gestalt_pushResonanceSignature(entry) {
    var e = entry || {};
    var sig = {
      id: global.GestaltResonanceSeq++,
      atPuls: puls(),
      sourceCycleId: parseInt(e.sourceCycleId, 10) || 0,
      goalImageId: parseInt(e.goalImageId, 10) || 0,
      problemTreeId: parseInt(e.problemTreeId, 10) || 0,
      situationId: parseInt(e.situationId, 10) || 0,
      perceptionNodeId: parseInt(e.perceptionNodeId, 10) || 0,
      automatismId: parseInt(e.automatismId, 10) || 0,
      actionImageId: parseInt(e.actionImageId, 10) || 0,
      effect: typeof e.effect === 'number' && !isNaN(e.effect) ? e.effect : null,
      kind: e.kind || 'episode_close'
    };
    global.GestaltResonanceBuffer.push(sig);
    while (global.GestaltResonanceBuffer.length > MAX_RESONANCE_BUFFER) {
      global.GestaltResonanceBuffer.shift();
    }
    markDirty();
    return sig;
  }

  /**
   * Эвристика сходства сигнатуры буфера с гештальтом (v1: скалярные совпадения ID).
   * Чем выше, тем больше «это про мою задачу» для открытого гештальта.
   */
  function scoreResonanceAgainstGestalt(sig, g) {
    var score = 0;
    if (sig.goalImageId && g.goalImageId === sig.goalImageId) score += 1.0;
    var branch = sig.perceptionNodeId || sig.problemTreeId;
    if (branch && g.problemTreeId && branch === g.problemTreeId) score += 0.28;
    if (sig.situationId && g.situationId && sig.situationId === g.situationId) score += 0.35;
    if (!g.situationId && sig.situationId) score += 0.08;
    if (sig.effect !== null && sig.effect > 0) score += 0.12;
    return score;
  }

  /**
   * Обход буфера резонанса: для записей с effect > 0 и открытых гештальтов — оценка; при score ≥ порога —
   * инсайт аналогии, дополнение automatismIds и analogyPool («гештальт-сборка», упрощённо).
   *
   * @returns {number} число срабатываний (пар сигнатура×гештальт), прошедших порог и изменивших инсайт.
   *
   * TODO: помечать записи буфера consumed, чтобы не обходить повторно при неизменном буфере.
   */
  function Gestalt_processResonanceBufferForCycle(cycle) {
    var buf = global.GestaltResonanceBuffer;
    if (!buf.length) return 0;

    var applied = 0;
    for (var b = 0; b < buf.length; b++) {
      var sig = buf[b];
      if (!sig || sig.effect === null || sig.effect <= 0) continue;

      for (var i = 0; i < global.GestaltArr.length; i++) {
        var g = global.GestaltArr[i];
        if (!isGestaltOpen(g)) continue;

        var sc = scoreResonanceAgainstGestalt(sig, g);
        if (sc < RESONANCE_SCORE_INSIGHT) continue;

        if (sig.automatismId) appendAutomatismIfNew(g, sig.automatismId);

        if (!Array.isArray(g.analogyPool)) g.analogyPool = [];
        g.analogyPool.push({
          resonanceId: sig.id,
          atPuls: sig.atPuls,
          score: sc,
          automatismId: sig.automatismId || 0,
          sourceCycleId: sig.sourceCycleId || 0
        });
        while (g.analogyPool.length > MAX_ANALOGY_POOL) g.analogyPool.shift();

        if (g.status === GESTALT_STATUS.NEW) {
          g.status = GESTALT_STATUS.STARTED;
          registerInsight(g, INSIGHT_KIND.ANALOGY, 'резонанс score=' + sc.toFixed(2) + ' resonanceId=' + sig.id, cycle);
          applied++;
        } else if (g.status === GESTALT_STATUS.STARTED) {
          registerInsight(g, INSIGHT_KIND.ANALOGY, 'уточнение аналогии score=' + sc.toFixed(2), cycle);
          applied++;
        }

        if (sig.situationId && !g.situationId) g.situationId = sig.situationId;
        var br = sig.perceptionNodeId || sig.problemTreeId;
        if (br && !g.problemTreeId) g.problemTreeId = br;
      }
    }
    return applied;
  }

  /**
   * Ведущий открытый гештальт по образу цели.
   * Приоритет goalImageId: поле цикла (сессия ожидания ЭП), иначе InfoPicture.currentGoalId при целевом режиме цикла.
   */
  function Gestalt_bindActiveMaster(cycle) {
    var goalId = 0;
    if (cycle && typeof cycle.goalImageId === 'number' && cycle.goalImageId > 0) {
      goalId = cycle.goalImageId;
    }
    var info = global.InfoPicture;
    var cycleTarget;
    if (cycle) {
      cycleTarget = cycle.thinkingMode !== 'passive';
    } else if (typeof global.Consciousness_getMainCycleThinkingMode === 'function') {
      cycleTarget = global.Consciousness_getMainCycleThinkingMode() !== 'passive';
    } else {
      cycleTarget = true;
    }
    if (!goalId && cycleTarget && info && typeof info.currentGoalId === 'number' && info.currentGoalId > 0) {
      goalId = info.currentGoalId;
    }
    if (!goalId) {
      global.Gestalt_activeGestaltId = 0;
      global.Gestalt_activeMasterCycleId = 0;
      return null;
    }

    var hint = {
      situationId: info && typeof info.situationId === 'number' ? info.situationId : 0,
      problemTreeId: (cycle && cycle.context && typeof cycle.context.branchId === 'number') ? cycle.context.branchId : 0
    };
    var g = findGestaltByGoal(goalId);
    if (!g) {
      g = Gestalt_createOrGetForGoal(goalId, hint);
    } else if (isGestaltOpen(g)) {
      if (hint.situationId && !g.situationId) g.situationId = hint.situationId;
      if (hint.problemTreeId && !g.problemTreeId) g.problemTreeId = hint.problemTreeId;
    }

    if (g && isGestaltOpen(g)) {
      global.Gestalt_activeGestaltId = g.id;
      global.Gestalt_activeMasterCycleId = cycle ? cycle.id : 0;
    } else {
      global.Gestalt_activeGestaltId = 0;
      global.Gestalt_activeMasterCycleId = cycle ? cycle.id : 0;
    }
    return g && isGestaltOpen(g) ? g : null;
  }

  /**
   * Начало ур.3: заполняет GestaltContextForLevel3. Лог при смене пары id+status (без спама каждый пульс).
   */
  function Gestalt_prepareContextForCycle(cycle) {
    if (!cycle || cycle.status !== 'running') {
      global.GestaltContextForLevel3 = null;
      return;
    }
    var g = Gestalt_bindActiveMaster(cycle);
    if (g && cycle.isMainCycle) {
      global.GestaltContextForLevel3 = {
        gestaltId: g.id,
        goalImageId: g.goalImageId,
        gestaltStatus: g.status,
        problemTreeId: g.problemTreeId,
        situationId: g.situationId
      };
      var snap = String(g.id) + '_' + String(g.status);
      if (cycle._gestaltPrepareLogSnap !== snap) {
        cycle._gestaltPrepareLogSnap = snap;
        logToCycle(cycle, 'Гештальт: контекст для ур.3 — id=' + g.id + ', goal=' + g.goalImageId +
          ', status=' + g.status + ', ветка=' + g.problemTreeId + ', ситуация=' + g.situationId + '.', false);
      }
    } else {
      global.GestaltContextForLevel3 = null;
    }
  }

  /**
   * Тик ур.4: только главный цикл. Резонанс, пока status < PARTIAL; затем обновление контекста.
   */
  function Gestalt_onConsciousnessTick(cycle) {
    if (!cycle || cycle.status !== 'running' || !cycle.isMainCycle) return;

    /** Для диспетчера: совпадение с cpuls на этом шаге → expired по стагнации после гештальта (см. consciousness_dispatcher). */
    cycle.lastGestaltConsciencePuls = puls();

    var g = Gestalt_bindActiveMaster(cycle);
    var applied = 0;
    if (g && g.status < GESTALT_STATUS.PARTIAL) {
      applied = Gestalt_processResonanceBufferForCycle(cycle);
    }
    if (applied > 0) {
      logToCycle(cycle, 'Ур.4/гештальт: резонанс — обработано срабатываний по буферу: ' + applied + '.', false);
    }

    Gestalt_prepareContextForCycle(cycle);
  }

  /**
   * Закрытие кадра ЭП: буфер резонанса + обновление гештальта по goalImageId цикла ожидания.
   */
  function Gestalt_onEpisodeFeedbackClosed(payload) {
    var p = payload || {};
    var waitCycleId = parseInt(p.waitCycleId, 10) || 0;
    var last = p.lastFrame || {};
    var effect = typeof p.effect === 'number' && !isNaN(p.effect) ? p.effect : null;
    var autoId = parseInt(p.automatismId, 10) || 0;

    var perceptionNodeId = last.perceptionNodeId || 0;
    var situationId = last.situationId || 0;

    var oc = waitCycleId && typeof global.Consciousness_getCycle === 'function'
      ? global.Consciousness_getCycle(waitCycleId) : null;
    var goalId = oc && typeof oc.goalImageId === 'number' ? oc.goalImageId : 0;

    var pushedSig = Gestalt_pushResonanceSignature({
      sourceCycleId: waitCycleId,
      goalImageId: goalId,
      problemTreeId: perceptionNodeId,
      situationId: situationId,
      perceptionNodeId: perceptionNodeId,
      automatismId: autoId,
      actionImageId: last.actionImageId || 0,
      effect: effect,
      kind: effect !== null && effect > 0 ? 'positive_feedback' : 'non_positive'
    });

    if (pushedSig) {
      var logC = oc || (typeof global.Consciousness_getMainCycle === 'function' ? global.Consciousness_getMainCycle() : null);
      var forceEp = !!(logC && (!logC.isMainCycle || logC.expired));
      logToCycle(logC, 'Гештальт/ЭП: резонанс — сигнатура #' + pushedSig.id +
        ' (цикл ожидания=' + waitCycleId + ', goal=' + pushedSig.goalImageId + ', effect=' + String(pushedSig.effect) +
        ', авто=' + pushedSig.automatismId + ', len буфера=' + global.GestaltResonanceBuffer.length + ').', forceEp);
    }

    if (!goalId) {
      if (oc) {
        logToCycle(oc, 'Гештальт/ЭП: goalImageId не задан — запись гештальта не менялась (сигнатура в буфере есть).', true);
      }
      return;
    }

    var g = findGestaltByGoal(goalId);
    if (!g) {
      g = Gestalt_createOrGetForGoal(goalId, {
        problemTreeId: perceptionNodeId,
        situationId: situationId
      });
    }
    if (!g) return;

    if (!isGestaltOpen(g)) {
      logToCycle(oc, 'Гештальт: цель ' + goalId + ' уже закрыта (status≥3) — смена статуса пропущена.', true);
      return;
    }

    if (autoId) {
      var added = appendAutomatismIfNew(g, autoId);
      if (added && oc) {
        logToCycle(oc, 'Гештальт #' + g.id + ': в список опробованных добавлен автоматизм id=' + autoId + '.', true);
      }
    }

    if (effect !== null && effect > 0) {
      if (!g.problemTreeId && perceptionNodeId) g.problemTreeId = perceptionNodeId;
      if (!g.situationId && situationId) g.situationId = situationId;

      if (effect >= EFFECT_STRONG_CLOSE) {
        g.status = GESTALT_STATUS.CLOSED_SUCCESS;
        registerInsight(g, INSIGHT_KIND.FEEDBACK, 'сильный эффект ≥' + String(EFFECT_STRONG_CLOSE) + ' — гештальт закрыт (effect=' + String(effect) + ')', oc);
      } else if (g.status <= GESTALT_STATUS.STARTED) {
        g.status = GESTALT_STATUS.PARTIAL;
        registerInsight(g, INSIGHT_KIND.FEEDBACK, 'effect=' + String(effect) + ' auto=' + String(autoId) + ' — status→PARTIAL', oc);
      }
    } else if (effect !== null && effect <= 0) {
      registerInsight(g, INSIGHT_KIND.PRE, 'effect≤0 — переинтерпретация? (effect=' + String(effect) + ')', oc);
    }
    markDirty();
  }

  /**
   * Ур.2, цель «значение»: если открытый гештальт про эту цель или перечисляет автоматизм с данным ОД — повысить статус / инсайт.
   */
  function Gestalt_bumpFromMeaningForecast(meaningGoalId, actionImageId, frame, cycleForLog) {
    var mg = parseInt(meaningGoalId, 10) || 0;
    var act = parseInt(actionImageId, 10) || 0;
    if (!mg || !act) return;

    function automatismMatchesAction(automatismId, odId) {
      var aid = parseInt(automatismId, 10) || 0;
      if (!aid || !Array.isArray(global.Automatizms)) return false;
      for (var ai = 0; ai < global.Automatizms.length; ai++) {
        var az = global.Automatizms[ai];
        if (az && az.id === aid && az.actionsImageId === odId) return true;
      }
      return false;
    }

    for (var i = 0; i < global.GestaltArr.length; i++) {
      var g = global.GestaltArr[i];
      if (!g || !isGestaltOpen(g)) continue;
      var match = g.goalImageId === mg;
      if (!match && Array.isArray(g.automatismIds)) {
        for (var j = 0; j < g.automatismIds.length; j++) {
          if (automatismMatchesAction(g.automatismIds[j], act)) {
            match = true;
            break;
          }
        }
      }
      if (!match) continue;

      if (g.status === GESTALT_STATUS.NEW) {
        g.status = GESTALT_STATUS.STARTED;
        registerInsight(g, INSIGHT_KIND.LOCAL, 'прогноз по смыслу стимула, ОД=' + act, cycleForLog);
      } else if (g.status === GESTALT_STATUS.STARTED) {
        g.status = GESTALT_STATUS.PARTIAL;
        registerInsight(g, INSIGHT_KIND.LOCAL, 'уточнение прогноза, ОД=' + act, cycleForLog);
      } else if (g.status === GESTALT_STATUS.PARTIAL) {
        registerInsight(g, INSIGHT_KIND.LOCAL, 'подкрепление прогноза, ОД=' + act, cycleForLog);
      }
    }
  }

  // ---- IndexedDB: сейв по dirty + сравнение подписи состояния ----
  var db = null;
  var DB_NAME = 'BEAST_Gestalt_DB';
  var DB_VERSION = 1;
  var STORE = 'gestalt_state';
  var KEY = 'gestalt_singleton';

  function serializeState() {
    return {
      gestaltArr: global.GestaltArr.map(function (g) { return normalizeGestalt(g); }),
      nextId: global.GestaltNextId,
      resonanceBuffer: global.GestaltResonanceBuffer.slice(),
      resonanceSeq: global.GestaltResonanceSeq,
      activeMasterCycleId: global.Gestalt_activeMasterCycleId,
      activeGestaltId: global.Gestalt_activeGestaltId
    };
  }

  function applyState(state) {
    if (!state || typeof state !== 'object') return;
    global.GestaltArr = Array.isArray(state.gestaltArr) ? state.gestaltArr.map(normalizeGestalt) : [];
    global.GestaltNextId = typeof state.nextId === 'number' && state.nextId > 0 ? state.nextId : 1;
    global.GestaltResonanceBuffer = Array.isArray(state.resonanceBuffer) ? state.resonanceBuffer : [];
    global.GestaltResonanceSeq = typeof state.resonanceSeq === 'number' && state.resonanceSeq > 0 ? state.resonanceSeq : 1;
    global.Gestalt_activeMasterCycleId = typeof state.activeMasterCycleId === 'number' ? state.activeMasterCycleId : 0;
    global.Gestalt_activeGestaltId = typeof state.activeGestaltId === 'number' ? state.activeGestaltId : 0;
    lastSavedSig = JSON.stringify(serializeState());
    gestaltDirty = false;
    if (typeof global.Info_updateView === 'function') {
      try {
        global.Info_updateView();
      } catch (eInfo) {}
    }
  }

  function openDb() {
    if (!global.indexedDB || !global.IndexedDBModule) return Promise.reject(new Error('IndexedDB не доступен'));
    if (db) return Promise.resolve(db);
    return global.IndexedDBModule.openDatabase({
      dbName: DB_NAME,
      version: DB_VERSION,
      stores: [{ name: STORE, options: { keyPath: 'id' } }]
    }).then(function (d) {
      db = d;
      return d;
    });
  }

  function saveToIndexedDB(state) {
    if (!global.IndexedDBModule) return;
    state = state || serializeState();
    function put(database) {
      return global.IndexedDBModule.put({
        db: database,
        storeName: STORE,
        value: { id: KEY, state: state, ts: Date.now() }
      }).catch(function () {});
    }
    if (db) put(db);
    else openDb().then(put).catch(function () {});
  }

  function loadFromIndexedDB() {
    if (!global.IndexedDBModule) return;
    openDb()
      .then(function (d) {
        return global.IndexedDBModule.get({ db: d, storeName: STORE, key: KEY });
      })
      .then(function (record) {
        if (record && record.state) applyState(record.state);
      })
      .catch(function () {});
  }

  if (typeof global.GestaltContextForLevel3 === 'undefined') {
    global.GestaltContextForLevel3 = null;
  }

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!gestaltDirty) return;
      var state = serializeState();
      var sig = JSON.stringify(state);
      if (sig === lastSavedSig) {
        gestaltDirty = false;
        return;
      }
      lastSavedSig = sig;
      gestaltDirty = false;
      saveToIndexedDB(state);
    });
  }

  loadFromIndexedDB();

  global.Gestalt_createOrGetForGoal = Gestalt_createOrGetForGoal;
  global.Gestalt_findGestaltByGoal = findGestaltByGoal;
  global.Gestalt_pushResonanceSignature = Gestalt_pushResonanceSignature;
  global.Gestalt_prepareContextForCycle = Gestalt_prepareContextForCycle;
  global.Gestalt_onConsciousnessTick = Gestalt_onConsciousnessTick;
  global.Gestalt_onEpisodeFeedbackClosed = Gestalt_onEpisodeFeedbackClosed;
  global.Gestalt_bindActiveMaster = Gestalt_bindActiveMaster;
  global.Gestalt_processResonanceBufferForCycle = Gestalt_processResonanceBufferForCycle;
  global.Gestalt_bumpFromMeaningForecast = Gestalt_bumpFromMeaningForecast;
})(window);
