import { BASE_IMAGE_CATEGORIES, userRequiresImageCache, waitForTokenTexture } from './utils.js';
import { ForgeSearchPaths } from '../applications/forgeSearchPaths.js';
import TokenHUDClientSettings from '../applications/tokenHUDClientSettings.js';
import CompendiumMapConfig from '../applications/compendiumMap.js';
import ImportExport from '../applications/importExport.js';
import ConfigureSettings from '../applications/configureSettings.js';
import { cacheImages, saveCache } from './search.js';
import { registerAllHooks } from './hooks/hooks.js';
import { registerAllWrappers } from './wrappers/wrappers.js';

export const TVA_CONFIG = {
  debug: false,
  disableNotifs: false,
  searchPaths: [
    {
      text: 'modules/caeora-maps-tokens-assets/assets/tokens',
      cache: true,
      source: typeof ForgeAPI === 'undefined' ? 'data' : 'forge-bazaar',
      types: ['Portrait', 'Token', 'PortraitAndToken'],
    },
  ],
  forgeSearchPaths: {},
  worldHud: {
    displayOnlySharedImages: false,
    disableIfTHWEnabled: false,
    includeKeywords: false,
    updateActorImage: false,
    useNameSimilarity: false,
    includeWildcard: true,
    showFullPath: false,
    animate: true,
  },
  hud: {
    enableSideMenu: true,
    displayAsImage: true,
    imageOpacity: 50,
  },
  keywordSearch: true,
  excludedKeywords: 'and,for',
  runSearchOnPath: false,
  searchFilters: {},
  algorithm: {
    exact: false,
    fuzzy: true,
    fuzzyLimit: 100,
    fuzzyThreshold: 0.3,
    fuzzyArtSelectPercentSlider: true,
  },
  tokenConfigs: [],
  randomizer: {
    actorCreate: false,
    tokenCreate: false,
    tokenCopyPaste: false,
    tokenName: true,
    keywords: false,
    shared: false,
    wildcard: false,
    representedActorDisable: false,
    linkedActorDisable: true,
    popupOnDisable: false,
    diffImages: false,
    syncImages: false,
    nonRepeat: false,
  },
  popup: {
    disableAutoPopupOnActorCreate: true,
    disableAutoPopupOnTokenCreate: true,
    disableAutoPopupOnTokenCopyPaste: true,
    twoPopups: false,
    twoPopupsNoDialog: false,
  },
  imgurClientId: '',
  stackStatusConfig: true,
  mergeGroup: false,
  staticCache: false,
  staticCacheFile: 'modules/token-variants/token-variants-cache.json',
  tilesEnabled: true,
  compendiumMapper: {
    missingOnly: false,
    diffImages: false,
    showImages: true,
    cache: false,
    autoDisplayArtSelect: true,
    syncImages: false,
    overrideCategory: false,
    category: 'Token',
    missingImages: [{ document: 'all', image: CONST.DEFAULT_TOKEN }],
    searchOptions: {},
  },
  permissions: {
    popups: {
      1: false,
      2: false,
      3: true,
      4: true,
    },
    portrait_right_click: {
      1: false,
      2: false,
      3: true,
      4: true,
    },
    image_path_button: {
      1: false,
      2: false,
      3: true,
      4: true,
    },
    hud: {
      1: true,
      2: true,
      3: true,
      4: true,
    },
    hudFullAccess: {
      1: false,
      2: false,
      3: true,
      4: true,
    },
    statusConfig: {
      1: false,
      2: false,
      3: true,
      4: true,
    },
  },
  globalMappings: [],
  templateMappings: [],
  customImageCategories: [],
  displayEffectIconsOnHover: false,
  disableEffectIcons: false,
  filterEffectIcons: false,
  filterCustomEffectIcons: true,
  filterIconList: [],
  updateTokenProto: false,
  imgNameContainsDimensions: false,
  imgNameContainsFADimensions: false,
  playVideoOnHover: true,
  pauseVideoOnHoverOut: false,
  disableImageChangeOnPolymorphed: false,
  disableImageUpdateOnNonPrototype: false,
  disableTokenUpdateAnimation: false,
  mappingsCurrentSceneOnly: false,
  evaluateOverlayOnHover: true,
  invisibleImage: '',
  systemHpPath: '',
  internalEffects: {
    hpChange: { enabled: false, duration: null },
  },
  hideElevationTooltip: false,
  hideTokenBorder: false,
};

export const FEATURE_CONTROL = {
  EffectMappings: true,
  EffectIcons: true,
  Overlays: true,
  UserMappings: true,
  Wildcards: true,
  PopUpAndRandomize: true,
  HUD: true,
  HideElement: true,
};

export function registerSettings() {
  game.settings.register('token-variants', 'featureControl', {
    scope: 'world',
    config: false,
    type: Object,
    default: FEATURE_CONTROL,
    onChange: async (val) => {
      mergeObject(FEATURE_CONTROL, val);
      registerAllHooks();
      registerAllWrappers();
    },
  });
  mergeObject(FEATURE_CONTROL, game.settings.get('token-variants', 'featureControl'));

  game.settings.registerMenu('token-variants', 'settings', {
    name: 'Configure Settings',
    hint: 'Configure Token Variant Art settings',
    label: 'Settings',
    scope: 'world',
    icon: 'fas fa-cog',
    type: ConfigureSettings,
    restricted: true,
  });

  const systemHpPaths = {
    'cyberpunk-red-core': 'derivedStats.hp',
    lfg: 'health',
    worldbuilding: 'health',
    twodsix: 'hits',
  };
  TVA_CONFIG.systemHpPath = systemHpPaths[game.system.id] ?? 'attributes.hp';

  game.settings.register('token-variants', 'effectMappingToggleGroups', {
    scope: 'world',
    config: false,
    type: Object,
    default: { Default: true },
  });

  game.settings.register('token-variants', 'settings', {
    scope: 'world',
    config: false,
    type: Object,
    default: TVA_CONFIG,
    onChange: async (val) => {
      // Generate a diff, it will be required when doing post-processing of the modified settings
      const diff = _arrayAwareDiffObject(TVA_CONFIG, val);

      // Check image re-cache required due to permission changes
      let requiresImageCache = false;
      if ('permissions' in diff) {
        if (!userRequiresImageCache(TVA_CONFIG.permissions) && userRequiresImageCache(val.permissions))
          requiresImageCache = true;
      }

      // Update live settings
      mergeObject(TVA_CONFIG, val);

      if (TVA_CONFIG.filterEffectIcons && ('filterCustomEffectIcons' in diff || 'filterIconList' in diff)) {
        for (const tkn of canvas.tokens.placeables) {
          waitForTokenTexture(tkn, (token) => {
            token.drawEffects();
          });
        }
      }

      // Check image re-cache required due to search path changes
      if ('searchPaths' in diff || 'forgeSearchPaths' in diff) {
        if (userRequiresImageCache(TVA_CONFIG.permissions)) requiresImageCache = true;
      }

      // Cache/re-cache images if necessary
      if (requiresImageCache) {
        await cacheImages();
      }

      if (diff.staticCache) {
        const cacheFile = diff.staticCacheFile ? diff.staticCacheFile : TVA_CONFIG.staticCacheFile;
        saveCache(cacheFile);
      }

      TVA_CONFIG.hud = game.settings.get('token-variants', 'hudSettings');

      registerAllHooks();
      registerAllWrappers();

      if ('displayEffectIconsOnHover' in diff) {
        for (const tkn of canvas.tokens.placeables) {
          if (tkn.effects) tkn.effects.visible = !diff.displayEffectIconsOnHover;
        }
      }

      if ('hideElevationTooltip' in diff) {
        for (const tkn of canvas.tokens.placeables) {
          if (tkn.tooltip) tkn.tooltip.text = tkn._getTooltipText();
        }
      }

      if ('hideTokenBorder' in diff) {
        for (const tkn of canvas.tokens.placeables) {
          if (tkn.border) tkn.border.visible = !diff.hideTokenBorder;
        }
      }

      if ('filterEffectIcons' in diff || 'disableEffectIcons' in diff) {
        for (const tkn of canvas.tokens.placeables) {
          tkn.drawEffects();
        }
      }
    },
  });

  game.settings.register('token-variants', 'debug', {
    scope: 'world',
    config: false,
    type: Boolean,
    default: TVA_CONFIG.debug,
    onChange: (val) => (TVA_CONFIG.debug = val),
  });

  if (typeof ForgeAPI !== 'undefined') {
    game.settings.registerMenu('token-variants', 'forgeSearchPaths', {
      name: game.i18n.localize('token-variants.settings.forge-search-paths.Name'),
      hint: game.i18n.localize('token-variants.settings.forge-search-paths.Hint'),
      icon: 'fas fa-search',
      type: ForgeSearchPaths,
      scope: 'client',
      restricted: false,
    });
  }

  game.settings.register('token-variants', 'tokenConfigs', {
    scope: 'world',
    config: false,
    type: Array,
    default: TVA_CONFIG.tokenConfigs,
    onChange: (val) => (TVA_CONFIG.tokenConfigs = val),
  });

  game.settings.registerMenu('token-variants', 'tokenHUDSettings', {
    name: game.i18n.localize('token-variants.settings.token-hud.Name'),
    hint: game.i18n.localize('token-variants.settings.token-hud.Hint'),
    scope: 'client',
    icon: 'fas fa-images',
    type: TokenHUDClientSettings,
    restricted: false,
  });

  game.settings.registerMenu('token-variants', 'compendiumMapper', {
    name: game.i18n.localize('token-variants.settings.compendium-mapper.Name'),
    hint: game.i18n.localize('token-variants.settings.compendium-mapper.Hint'),
    scope: 'world',
    icon: 'fas fa-cogs',
    type: CompendiumMapConfig,
    restricted: true,
  });

  game.settings.register('token-variants', 'compendiumMapper', {
    scope: 'world',
    config: false,
    type: Object,
    default: TVA_CONFIG.compendiumMapper,
    onChange: (val) => (TVA_CONFIG.compendiumMapper = val),
  });

  game.settings.register('token-variants', 'hudSettings', {
    scope: 'client',
    config: false,
    type: Object,
    default: TVA_CONFIG.hud,
    onChange: (val) => (TVA_CONFIG.hud = val),
  });

  game.settings.registerMenu('token-variants', 'importExport', {
    name: `Import/Export`,
    hint: game.i18n.localize('token-variants.settings.import-export.Hint'),
    scope: 'world',
    icon: 'fas fa-toolbox',
    type: ImportExport,
    restricted: true,
  });

  // Read settings
  const settings = game.settings.get('token-variants', 'settings');
  mergeObject(TVA_CONFIG, settings);

  if (isEmpty(TVA_CONFIG.searchFilters)) {
    BASE_IMAGE_CATEGORIES.forEach((cat) => {
      TVA_CONFIG.searchFilters[cat] = {
        include: '',
        exclude: '',
        regex: '',
      };
    });
  }

  for (let uid in TVA_CONFIG.forgeSearchPaths) {
    TVA_CONFIG.forgeSearchPaths[uid].paths = TVA_CONFIG.forgeSearchPaths[uid].paths.map((p) => {
      if (!p.source) {
        p.source = 'forgevtt';
      }
      if (!p.types) {
        if (p.tiles) p.types = ['Tile'];
        else p.types = ['Portrait', 'Token', 'PortraitAndToken'];
      }
      return p;
    });
  }

  // 20/07/2023 Convert globalMappings to a new format
  if (getType(settings.globalMappings) === 'Object') {
    Hooks.once('ready', () => {
      TVA_CONFIG.globalMappings = migrateMappings(settings.globalMappings);
      setTimeout(() => updateSettings({ globalMappings: TVA_CONFIG.globalMappings }), 10000);
    });
  }

  // Read client settings
  TVA_CONFIG.hud = game.settings.get('token-variants', 'hudSettings');
}

export function migrateMappings(mappings, globalMappings = []) {
  if (!mappings) return [];
  if (getType(mappings) === 'Object') {
    let nMappings = [];
    for (const [effect, mapping] of Object.entries(mappings)) {
      if (!mapping.label) mapping.label = effect.replaceAll('¶', '.');
      if (!mapping.expression) mapping.expression = effect.replaceAll('¶', '.');
      if (!mapping.id) mapping.id = randomID(8);
      delete mapping.effect;
      if (mapping.overlayConfig) mapping.overlayConfig.id = mapping.id;
      delete mapping.overlayConfig?.effect;
      nMappings.push(mapping);
    }
    // Convert parents to parentIDs
    let combMappings = nMappings.concat(globalMappings);
    for (const mapping of nMappings) {
      if (mapping.overlayConfig?.parent) {
        if (mapping.overlayConfig.parent === 'Token (Placeable)') {
          mapping.overlayConfig.parentID = 'TOKEN';
        } else {
          const parent = combMappings.find((m) => m.label === mapping.overlayConfig.parent);
          if (parent) mapping.overlayConfig.parentID = parent.id;
          else mapping.overlayConfig.parentID = '';
        }
        delete mapping.overlayConfig.parent;
      }
    }
    return nMappings;
  }
  return mappings;
}

export function getFlagMappings(object) {
  if (!object) return [];
  let doc = object.document ?? object;
  const actorId = doc.actor?.id;
  if (actorId) {
    doc = game.actors.get(actorId);
    if (!doc) return [];
  }

  // 23/07/2023
  let mappings = doc.getFlag('token-variants', 'effectMappings') ?? [];
  if (getType(mappings) === 'Object') {
    mappings = migrateMappings(mappings, TVA_CONFIG.globalMappings);
    doc.setFlag('token-variants', 'effectMappings', mappings);
  }
  return mappings;
}

export function exportSettingsToJSON() {
  const settings = deepClone(TVA_CONFIG);
  const filename = `token-variants-settings.json`;
  saveDataToFile(JSON.stringify(settings, null, 2), 'text/json', filename);
}

export async function importSettingsFromJSON(json) {
  if (typeof json === 'string') json = JSON.parse(json);

  if (json.forgeSearchPaths)
    for (let uid in json.forgeSearchPaths) {
      json.forgeSearchPaths[uid].paths = json.forgeSearchPaths[uid].paths.map((p) => {
        if (!p.source) {
          p.source = 'forgevtt';
        }
        if (!p.types) {
          if (p.tiles) p.types = ['Tile'];
          else p.types = ['Portrait', 'Token', 'PortraitAndToken'];
        }
        return p;
      });
    }

  // 09/07/2022 Convert filters to new format if old one is still in use
  if (json.searchFilters && json.searchFilters.portraitFilterInclude != null) {
    const filters = json.searchFilters;
    json.searchFilters = {
      Portrait: {
        include: filters.portraitFilterInclude ?? '',
        exclude: filters.portraitFilterExclude ?? '',
        regex: filters.portraitFilterRegex ?? '',
      },
      Token: {
        include: filters.tokenFilterInclude ?? '',
        exclude: filters.tokenFilterExclude ?? '',
        regex: filters.tokenFilterRegex ?? '',
      },
      PortraitAndToken: {
        include: filters.generalFilterInclude ?? '',
        exclude: filters.generalFilterExclude ?? '',
        regex: filters.generalFilterRegex ?? '',
      },
    };
    if (json.compendiumMapper) delete json.compendiumMapper.searchFilters;
  }

  // Global Mappings need special merge
  if (json.globalMappings) {
    const nMappings = migrateMappings(json.globalMappings);
    for (const m of nMappings) {
      const i = TVA_CONFIG.globalMappings.findIndex((mapping) => m.label === mapping.label);
      if (i === -1) TVA_CONFIG.globalMappings.push(m);
      else TVA_CONFIG.globalMappings[i] = m;
    }
    json.globalMappings = TVA_CONFIG.globalMappings;
  }

  updateSettings(json);
}

function _refreshFilters(filters, customCategories, updateTVAConfig = false) {
  const categories = BASE_IMAGE_CATEGORIES.concat(customCategories ?? TVA_CONFIG.customImageCategories);
  for (const filter in filters) {
    if (!categories.includes(filter)) {
      delete filters[filter];
      if (updateTVAConfig) delete TVA_CONFIG.searchFilters[filter];
    }
  }
  for (const category of customCategories) {
    if (filters[category] == null) {
      filters[category] = {
        include: '',
        exclude: '',
        regex: '',
      };
    }
  }
}

export async function updateSettings(newSettings) {
  const settings = mergeObject(deepClone(TVA_CONFIG), newSettings, { insertKeys: false });
  // Custom image categories might have changed, meaning we may have filters that are no longer relevant
  // or need to be added
  if ('customImageCategories' in newSettings) {
    _refreshFilters(settings.searchFilters, newSettings.customImageCategories, true);
    if (settings.compendiumMapper?.searchOptions?.searchFilters != null) {
      _refreshFilters(settings.compendiumMapper.searchOptions.searchFilters, newSettings.customImageCategories);
      TVA_CONFIG.compendiumMapper.searchOptions.searchFilters = settings.compendiumMapper.searchOptions.searchFilters;
    }
  }
  await game.settings.set('token-variants', 'settings', settings);
}

export function _arrayAwareDiffObject(original, other, { inner = false } = {}) {
  function _difference(v0, v1) {
    let t0 = getType(v0);
    let t1 = getType(v1);
    if (t0 !== t1) return [true, v1];
    if (t0 === 'Array') return [!_arrayEquality(v0, v1), v1];
    if (t0 === 'Object') {
      if (isEmpty(v0) !== isEmpty(v1)) return [true, v1];
      let d = _arrayAwareDiffObject(v0, v1, { inner });
      return [!isEmpty(d), d];
    }
    return [v0 !== v1, v1];
  }

  // Recursively call the _difference function
  return Object.keys(other).reduce((obj, key) => {
    if (inner && !(key in original)) return obj;
    let [isDifferent, difference] = _difference(original[key], other[key]);
    if (isDifferent) obj[key] = difference;
    return obj;
  }, {});
}

function _arrayEquality(a1, a2) {
  if (!(a2 instanceof Array) || a2.length !== a1.length) return false;
  return a1.every((v, i) => {
    if (getType(v) === 'Object') return Object.keys(_arrayAwareDiffObject(v, a2[i])).length === 0;
    return a2[i] === v;
  });
}

export function getSearchOptions() {
  return {
    keywordSearch: TVA_CONFIG.keywordSearch,
    excludedKeywords: TVA_CONFIG.excludedKeywords,
    runSearchOnPath: TVA_CONFIG.runSearchOnPath,
    algorithm: TVA_CONFIG.algorithm,
    searchFilters: TVA_CONFIG.searchFilters,
  };
}
