Source: settings/schema.js

/**
 * User settings: the options a viewer remembers between visits.
 *
 * Saved settings are always merged over DEFAULT_SETTINGS and validated key by key, so:
 *   - new options added in a later release start at their default without touching the rest,
 *   - options removed in a later release are dropped quietly,
 *   - a bad or hand-edited value falls back to the default instead of breaking the chart.
 * Bump SETTINGS_VERSION and add a step to MIGRATIONS only when a key changes meaning or name.
 */

export const SETTINGS_VERSION = 1;

const oneOf = (...values) => (v) => values.includes(v);
const isBool = (v) => typeof v === 'boolean';
const PRESET_VALUES = {
  WindLapse: ['auto', 'Default', 'Severe', 'Dryline', 'Elevated', 'Winter', 'Fire'],
  Inset: ['auto', 'Default', 'Severe', 'Ambient', 'Winter', 'Fire'],
  Vertical: ['auto', 'Default', 'Winter', 'Fire'],
  SidePanel: ['auto', 'Graph', 'Tornado', 'Winter'],
};

/** key → validator. Every persisted setting must be listed here. */
export const SETTINGS_SCHEMA = {
  units: oneOf('default', 'metric', 'imperial'),
  skewTSkew: oneOf(0.5, 0.625, 0.75, 0.825),
  hodographNumbers: isBool,
  hodographColorTable: oneOf(0, 1, 2, 3),
  hodographScaleMode: oneOf(0, 1, 2),
  hodographTrimWindProfile: isBool,
  hodographMode: oneOf('Relative', 'Storm'),
  readOut: isBool,
  verticalBarsWindSpeed: isBool,
  showStormMotions: isBool,
  presetOverrides: (v) =>
    v !== null && typeof v === 'object' &&
    Object.entries(PRESET_VALUES).every(([k, allowed]) => v[k] === undefined || allowed.includes(v[k])),
  efScale: oneOf('EF2+', 'EF0+'),
  defaultParcel: oneOf('MU', 'SB', 'ML'),
  defaultMotion: oneOf('RM', 'LM', 'MM', 'US', 'DS', 'DTMR', 'DTML'),
  exportFormat: oneOf('Default', 'SharpJS', 'CM1'),
  // Controls-panel preferences (not used by the chart engine itself)
  surfaceTempUnit: oneOf('F', 'C', 'K'),
  lapseLayerMode: oneOf('Hght', 'Pres'),
  shearLayerMode: oneOf('Hght', 'Pres'),
  collapsedSections: (v) => Array.isArray(v) && v.every((s) => typeof s === 'string') && v.length < 50,
};

export const DEFAULT_SETTINGS = Object.freeze({
  units: 'default',
  skewTSkew: 0.75,
  hodographNumbers: false,
  hodographColorTable: 0,
  hodographScaleMode: 0,
  hodographTrimWindProfile: true,
  hodographMode: 'Relative',
  readOut: true,
  verticalBarsWindSpeed: true,
  showStormMotions: false,
  presetOverrides: Object.freeze({ WindLapse: 'auto', Inset: 'auto', Vertical: 'auto', SidePanel: 'auto' }),
  efScale: 'EF2+',
  defaultParcel: 'MU',
  defaultMotion: 'RM',
  exportFormat: 'Default',
  surfaceTempUnit: 'F',
  lapseLayerMode: 'Hght',
  shearLayerMode: 'Hght',
  collapsedSections: Object.freeze([]),
});

export { PRESET_VALUES };

/** Steps from version n to n + 1. MIGRATIONS[1] would upgrade a v1 payload to v2. */
const MIGRATIONS = {};

/** Upgrade a stored payload ({ v, settings }) to the current version. */
export function migrate(payload) {
  if (!payload || typeof payload !== 'object') return {};
  let version = Number.isInteger(payload.v) ? payload.v : 1;
  let settings = payload.settings && typeof payload.settings === 'object' ? { ...payload.settings } : {};
  while (version < SETTINGS_VERSION) {
    const step = MIGRATIONS[version];
    if (step) settings = step(settings);
    version += 1;
  }
  return settings;
}

/**
 * Deep-copy JSON-compatible values while leaving primitives unchanged.
 * @param {*} value Value to copy.
 * @returns {*} A JSON-deep copy of objects or the original primitive value.
 */
function clone(value) {
  return value && typeof value === 'object' ? JSON.parse(JSON.stringify(value)) : value;
}

/**
 * Merge `input` over `base` keeping only known keys with valid values.
 * presetOverrides is merged per preset so a partial object is fine.
 */
export function sanitizeSettings(input, base = DEFAULT_SETTINGS) {
  const out = clone(base);
  if (!input || typeof input !== 'object') return out;
  for (const [key, valid] of Object.entries(SETTINGS_SCHEMA)) {
    if (!(key in input)) continue;
    let value = input[key];
    if (key === 'presetOverrides' && value && typeof value === 'object') {
      value = { ...out.presetOverrides, ...value };
    }
    if (valid(value)) out[key] = clone(value);
  }
  return out;
}

/** Only the keys whose values differ from DEFAULT_SETTINGS (what actually needs storing). */
export function diffFromDefaults(settings) {
  const out = {};
  for (const key of Object.keys(SETTINGS_SCHEMA)) {
    if (JSON.stringify(settings[key]) !== JSON.stringify(DEFAULT_SETTINGS[key])) out[key] = clone(settings[key]);
  }
  return out;
}