import { isInitialized } from '../token-variants.mjs';
import { Fuse } from './fuse/fuse.js';
import { getSearchOptions, TVA_CONFIG } from './settings.js';
import {
  callForgeVTT,
  decodeURIComponentSafely,
  decodeURISafely,
  flattenSearchResults,
  getFileName,
  getFileNameWithExt,
  getFilePath,
  getFilters,
  isImage,
  isVideo,
  parseKeywords,
  SEARCH_TYPE,
  simplifyName,
  simplifyPath,
} from './utils.js';

// True if in the middle of caching image paths
let caching = false;
export function isCaching() {
  return caching;
}

// Cached images
let CACHED_IMAGES = {};

/**
 * @param {string} search Text to be used as the search criteria
 * @param {object} [options={}] Options which customize the search
 * @param {SEARCH_TYPE|string} [options.searchType] Controls filters applied to the search results
 * @param {Boolean} [options.simpleResults] Results will be returned as an array of all image paths found
 * @param {Function[]} [options.callback] Function to be called with the found images
 * @param {object} [options.searchOptions] Override search settings
 * @returns {Promise<Map<string, Array<object>|Array<string>>} All images found split by original criteria and keywords
 */
export async function doImageSearch(
  search,
  { searchType = SEARCH_TYPE.PORTRAIT_AND_TOKEN, simpleResults = false, callback = null, searchOptions = {} } = {}
) {
  if (caching) return;

  searchOptions = mergeObject(searchOptions, getSearchOptions(), { overwrite: false });

  search = search.trim();

  if (TVA_CONFIG.debug) console.info('TVA | STARTING: Art Search', search, searchType, searchOptions);

  let searches = [search];
  let allImages = new Map();
  const keywords = parseKeywords(searchOptions.excludedKeywords);

  if (searchOptions.keywordSearch) {
    searches = searches.concat(
      search
        .split(/[_\- :,\|\(\)\[\]]/)
        .filter((word) => word.length > 2 && !keywords.includes(word.toLowerCase()))
        .reverse()
    );
  }

  let usedImages = new Set();
  for (const search of searches) {
    if (allImages.get(search) !== undefined) continue;

    let results = await findImages(search, searchType, searchOptions);
    results = results.filter((pathObj) => !usedImages.has(pathObj));

    allImages.set(search, results);
    results.forEach(usedImages.add, usedImages);
  }

  if (TVA_CONFIG.debug) console.info('TVA | ENDING: Art Search');

  if (simpleResults) {
    allImages = Array.from(usedImages).map((obj) => obj.path);
  }

  if (callback) callback(allImages);
  return allImages;
}

/**
 * @param {*} search Text to be used as the search criteria
 * @param {object} [options={}] Options which customize the search
 * @param {SEARCH_TYPE|string} [options.searchType] Controls filters applied to the search results
 * @param {Actor} [options.actor] Used to retrieve 'shared' images from if enabled in the Randomizer Settings
 * @param {Function[]} [options.callback] Function to be called with the random image
 * @param {object} [options.searchOptions] Override search settings
 * @param {object} [options.randomizerOptions] Override randomizer settings. These take precedence over searchOptions
 * @returns Array<string>|null} Image path and name
 */
export async function doRandomSearch(
  search,
  {
    searchType = SEARCH_TYPE.PORTRAIT_AND_TOKEN,
    actor = null,
    callback = null,
    randomizerOptions = {},
    searchOptions = {},
  } = {}
) {
  if (caching) return null;

  let results = flattenSearchResults(
    await _randSearchUtil(search, {
      searchType: searchType,
      actor: actor,
      randomizerOptions: randomizerOptions,
      searchOptions: searchOptions,
    })
  );
  if (results.length === 0) return null;

  let result;

  // If `nonRepeat` option is enabled keep attempting random selection until a unique token image is found
  // in case of no such image, just pick a random one
  if (results.length !== 1 && randomizerOptions.nonRepeat && searchType === SEARCH_TYPE.TOKEN) {
    const tokens = canvas.tokens?.placeables || [];
    const placedImages = new Set(tokens.map((t) => t.document.texture.src));
    let checkedImages = [];
    let tmpResult = results[Math.floor(Math.random() * results.length)];

    while (results.length && !result) {
      if (placedImages.has(tmpResult.path)) {
        checkedImages.push(tmpResult);
        results.splice(results.indexOf(tmpResult), 1);
        tmpResult = results[Math.floor(Math.random() * results.length)];
      } else {
        result = tmpResult;
      }
    }

    if (!result) results = checkedImages;
  }

  if (!result) {
    result = results[Math.floor(Math.random() * results.length)];
  }

  // Pick random image
  if (callback) callback([result.path, result.name]);
  return [result.path, result.name];
}

export async function doSyncSearch(target, { searchType = SEARCH_TYPE.TOKEN } = {}) {
  if (caching) return null;

  const fResults = await findImages(target, searchType, { algorithm: { fuzzy: true } });

  if (fResults && fResults.length !== 0) {
    return [fResults[0].path, fResults[0].name];
  } else {
    return null;
  }
}

async function _randSearchUtil(
  search,
  { searchType = SEARCH_TYPE.PORTRAIT_AND_TOKEN, actor = null, randomizerOptions = {}, searchOptions = {} } = {}
) {
  const randSettings = mergeObject(randomizerOptions, TVA_CONFIG.randomizer, { overwrite: false });
  if (
    !(
      randSettings.tokenName ||
      randSettings.actorName ||
      randSettings.keywords ||
      randSettings.shared ||
      randSettings.wildcard
    )
  )
    return null;

  // Randomizer settings take precedence
  searchOptions.keywordSearch = randSettings.keywords;

  // Swap search to the actor name if need be
  if (randSettings.actorName && actor) {
    search = actor.name;
  }

  // Gather all images
  let results =
    randSettings.actorName || randSettings.tokenName || randSettings.keywords
      ? await doImageSearch(search, {
          searchType: searchType,
          searchOptions: searchOptions,
        })
      : new Map();

  if (!randSettings.tokenName && !randSettings.actorName) {
    results.delete(search);
  }
  if (randSettings.shared && actor) {
    let sharedVariants = actor.getFlag('token-variants', 'variants') || [];
    if (sharedVariants.length != 0) {
      const sv = [];
      sharedVariants.forEach((variant) => {
        variant.names.forEach((name) => {
          sv.push({ path: variant.imgSrc, name: name });
        });
      });
      results.set('variants95436723', sv);
    }
  }
  if (randSettings.wildcard && actor) {
    let protoImg = actor.prototypeToken.texture.src;
    if (protoImg.includes('*') || (protoImg.includes('{') && protoImg.includes('}'))) {
      // Modified version of Actor.getTokenImages()
      const getTokenImages = async (actor) => {
        if (actor._tokenImages) return actor._tokenImages;
        let source = 'data';
        const browseOptions = { wildcard: true };

        // Support non-user sources
        if (/\.s3\./.test(protoImg)) {
          source = 's3';
          const { bucket, keyPrefix } = FilePicker.parseS3URL(protoImg);
          if (bucket) {
            browseOptions.bucket = bucket;
            protoImg = keyPrefix;
          }
        } else if (protoImg.startsWith('icons/')) source = 'public';

        // Retrieve wildcard content
        try {
          const content = await FilePicker.browse(source, protoImg, browseOptions);
          return content.files;
        } catch (err) {
          return [];
        }
      };

      const wildcardImages = (await getTokenImages(actor))
        .filter((img) => !img.includes('*') && (isImage(img) || isVideo(img)))
        .map((variant) => {
          return { path: variant, name: getFileName(variant) };
        });
      results.set('variants95436623', wildcardImages);
    }
  }

  return results;
}

/**
 * Recursive image search through a directory
 * @param {*} path starting path
 * @param {*} options.apiKey ForgeVTT AssetLibrary API key
 * @param {*} found_images all the images found
 * @returns void
 */
async function walkFindImages(path, { apiKey = '' } = {}, found_images) {
  let files = {};
  if (!path.source) {
    path.source = 'data';
  }
  const typeKey = path.types.sort().join(',');
  try {
    if (path.source.startsWith('s3:')) {
      files = await FilePicker.browse('s3', path.text, {
        bucket: path.source.replace('s3:', ''),
      });
    } else if (path.source.startsWith('forgevtt')) {
      if (apiKey) {
        const response = await callForgeVTT(path.text, apiKey);
        files.files = response.files.map((f) => f.url);
      } else {
        files = await FilePicker.browse('forgevtt', path.text, { recursive: true });
      }
    } else if (path.source.startsWith('forge-bazaar')) {
      files = await FilePicker.browse('forge-bazaar', path.text, { recursive: true });
    } else if (path.source.startsWith('imgur')) {
      await fetch('https://api.imgur.com/3/gallery/album/' + path.text, {
        headers: {
          Authorization: 'Client-ID ' + (TVA_CONFIG.imgurClientId ? TVA_CONFIG.imgurClientId : 'df9d991443bb222'),
          Accept: 'application/json',
        },
      })
        .then((response) => response.json())
        .then(async function (result) {
          if (!result.success) {
            return;
          }
          result.data.images.forEach((img) => {
            const rtName = img.title ?? img.description ?? getFileName(img.link);
            _addToFound({ path: decodeURISafely(img.link), name: rtName }, typeKey, found_images);
          });
        })
        .catch((error) => console.warn('TVA |', error));
      return;
    } else if (path.source.startsWith('rolltable')) {
      const table = game.tables.contents.find((t) => t.name === path.text);
      if (!table) {
        const rollTableName = path.text;
        ui.notifications.warn(
          game.i18n.format('token-variants.notifications.warn.invalid-table', {
            rollTableName,
          })
        );
      } else {
        for (let baseTableData of table.results) {
          const rtPath = baseTableData.img;
          const rtName = baseTableData.text || getFileName(rtPath);
          _addToFound({ path: decodeURISafely(rtPath), name: rtName }, typeKey, found_images);
        }
      }
      return;
    } else if (path.source.startsWith('json')) {
      await fetch(path.text, {
        headers: {
          Accept: 'application/json',
        },
      })
        .then((response) => response.json())
        .then(async function (result) {
          if (!result.length > 0) {
            return;
          }
          result.forEach((img) => {
            const rtName = img.name ?? getFileName(img.path);
            _addToFound({ path: decodeURISafely(img.path), name: rtName, tags: img.tags }, typeKey, found_images);
          });
        })
        .catch((error) => console.warn('TVA |', error));
      return;
    } else {
      files = await FilePicker.browse(path.source, path.text);
    }
  } catch (err) {
    console.warn(
      `TVA | ${game.i18n.localize('token-variants.notifications.warn.path-not-found')} ${path.source}:${path.text}`
    );
    return;
  }

  if (files.target == '.') return;

  if (files.files) {
    files.files.forEach((tokenSrc) => {
      _addToFound({ path: decodeURISafely(tokenSrc), name: getFileName(tokenSrc) }, typeKey, found_images);
    });
  }

  // ForgeVTT requires special treatment
  // Bazaar paths fail recursive search if one level above root
  if (path.source.startsWith('forgevtt')) return;
  else if (
    path.source.startsWith('forge-bazaar') &&
    !['modules', 'systems', 'worlds', 'assets'].includes(path.text.replaceAll(/[\/\\]/g, ''))
  ) {
    return;
  }

  for (let f_dir of files.dirs) {
    await walkFindImages({ text: f_dir, source: path.source, types: path.types }, { apiKey: apiKey }, found_images);
  }
}

function _addToFound(img, typeKey, found_images) {
  if (isImage(img.path) || isVideo(img.path)) {
    if (found_images[typeKey] == null) {
      found_images[typeKey] = [img];
    } else {
      found_images[typeKey].push(img);
    }
  }
}

/**
 * Recursive walks through all paths exposed to the module and caches them
 * @param {*} searchType
 * @returns
 */
async function walkAllPaths(searchType) {
  const found_images = {};
  const paths = _filterPathsByType(TVA_CONFIG.searchPaths, searchType);

  for (const path of paths) {
    if ((path.cache && caching) || (!path.cache && !caching)) await walkFindImages(path, {}, found_images);
  }

  // ForgeVTT specific path handling
  const userId = typeof ForgeAPI !== 'undefined' ? await ForgeAPI.getUserId() : '';
  for (const uid in TVA_CONFIG.forgeSearchPaths) {
    const apiKey = TVA_CONFIG.forgeSearchPaths[uid].apiKey;
    const paths = _filterPathsByType(TVA_CONFIG.forgeSearchPaths[uid].paths, searchType);
    if (uid === userId) {
      for (const path of paths) {
        if ((path.cache && caching) || (!path.cache && !caching)) await walkFindImages(path, {}, found_images);
      }
    } else if (apiKey) {
      for (const path of paths) {
        if ((path.cache && caching) || (!path.cache && !caching)) {
          if (path.share) await walkFindImages(path, { apiKey: apiKey }, found_images);
        }
      }
    }
  }

  return found_images;
}

function _filterPathsByType(paths, searchType) {
  if (!searchType) return paths;
  return paths.filter((p) => p.types.includes(searchType));
}

export async function findImagesFuzzy(name, searchType, searchOptions, forceSearchName = false) {
  if (TVA_CONFIG.debug)
    console.info('TVA | STARTING: Fuzzy Image Search', name, searchType, searchOptions, forceSearchName);

  const filters = getFilters(searchType, searchOptions.searchFilters);

  const fuse = new Fuse([], {
    keys: [!forceSearchName && searchOptions.runSearchOnPath ? 'path' : 'name', 'tags'],
    includeScore: true,
    includeMatches: true,
    minMatchCharLength: 1,
    ignoreLocation: true,
    threshold: searchOptions.algorithm.fuzzyThreshold,
  });

  const found_images = await walkAllPaths(searchType);

  for (const container of [CACHED_IMAGES, found_images]) {
    for (const typeKey in container) {
      const types = typeKey.split(',');
      if (types.includes(searchType)) {
        for (const imgObj of container[typeKey]) {
          if (_imagePassesFilter(imgObj.name, imgObj.path, filters, searchOptions.runSearchOnPath)) {
            fuse.add(imgObj);
          }
        }
      }
    }
  }

  let results;
  if (name === '') {
    results = fuse.getIndex().docs.slice(0, searchOptions.algorithm.fuzzyLimit);
  } else {
    results = fuse.search(name).slice(0, searchOptions.algorithm.fuzzyLimit);
    results = results.map((r) => {
      r.item.indices = r.matches[0].indices;
      r.item.score = r.score;
      return r.item;
    });
  }

  if (TVA_CONFIG.debug) console.info('TVA | ENDING: Fuzzy Image Search', results);

  return results;
}

async function findImagesExact(name, searchType, searchOptions) {
  if (TVA_CONFIG.debug) console.info('TVA | STARTING: Exact Image Search', name, searchType, searchOptions);

  const found_images = await walkAllPaths(searchType);

  const simpleName = simplifyName(name);
  const filters = getFilters(searchType, searchOptions.searchFilters);

  const matchedImages = [];

  for (const container of [CACHED_IMAGES, found_images]) {
    for (const typeKey in container) {
      const types = typeKey.split(',');
      if (types.includes(searchType)) {
        for (const imgOBj of container[typeKey]) {
          if (_exactSearchMatchesImage(simpleName, imgOBj.path, imgOBj.name, filters, searchOptions.runSearchOnPath)) {
            matchedImages.push(imgOBj);
          }
        }
      }
    }
  }

  if (TVA_CONFIG.debug) console.info('TVA | ENDING: Exact Image Search', matchedImages);
  return matchedImages;
}

async function findImages(name, searchType = '', searchOptions = {}) {
  const sOptions = mergeObject(searchOptions, getSearchOptions(), { overwrite: false });
  if (sOptions.algorithm.exact) {
    return await findImagesExact(name, searchType, sOptions);
  } else {
    return await findImagesFuzzy(name, searchType, sOptions);
  }
}

/**
 * Checks if image path and name match the provided search text and filters
 * @param imagePath image path
 * @param imageName image name
 * @param filters filters to be applied
 * @returns true|false
 */
function _exactSearchMatchesImage(simplifiedSearch, imagePath, imageName, filters, runSearchOnPath) {
  // Is the search text contained in the name/path
  const simplified = runSearchOnPath ? simplifyPath(imagePath) : simplifyName(imageName);
  if (!simplified.includes(simplifiedSearch)) {
    return false;
  }

  if (!filters) return true;
  return _imagePassesFilter(imageName, imagePath, filters, runSearchOnPath);
}

function _imagePassesFilter(imageName, imagePath, filters, runSearchOnPath) {
  // Filters are applied to path depending on the 'runSearchOnPath' setting, and actual or custom rolltable name
  let text;
  if (runSearchOnPath) {
    text = decodeURIComponentSafely(imagePath);
  } else if (getFileName(imagePath) === imageName) {
    text = getFileNameWithExt(imagePath);
  } else {
    text = imageName;
  }

  if (filters.regex) {
    return filters.regex.test(text);
  }
  if (filters.include) {
    if (!text.includes(filters.include)) return false;
  }
  if (filters.exclude) {
    if (text.includes(filters.exclude)) return false;
  }
  return true;
}

// ===================================
// ==== CACHING RELATED FUNCTIONS ====
// ===================================

export async function saveCache(cacheFile) {
  const data = {};

  const caches = Object.keys(CACHED_IMAGES);
  for (const c of caches) {
    if (!(c in data)) data[c] = [];
    for (const img of CACHED_IMAGES[c]) {
      if (img.tags) {
        data[c].push([img.path, img.name, img.tags]);
      } else if (getFileName(img.path) === img.name) {
        data[c].push(img.path);
      } else {
        data[c].push([img.path, img.name]);
      }
    }
  }

  let file = new File([JSON.stringify(data)], getFileNameWithExt(cacheFile), {
    type: 'text/plain',
  });
  FilePicker.upload('data', getFilePath(cacheFile), file);
}

/**
 * Search for and cache all the found token art
 */
export async function cacheImages({
  staticCache = TVA_CONFIG.staticCache,
  staticCacheFile = TVA_CONFIG.staticCacheFile,
} = {}) {
  if (caching) return;
  caching = true;

  if (!isInitialized() && staticCache) {
    if (await _readCacheFromFile(staticCacheFile)) {
      caching = false;
      return;
    }
  }

  if (!TVA_CONFIG.disableNotifs)
    ui.notifications.info(game.i18n.format('token-variants.notifications.info.caching-started'));

  if (TVA_CONFIG.debug) console.info('TVA | STARTING: Token Caching');
  const found_images = await walkAllPaths();
  CACHED_IMAGES = found_images;

  if (TVA_CONFIG.debug) console.info('TVA | ENDING: Token Caching');

  caching = false;
  if (!TVA_CONFIG.disableNotifs)
    ui.notifications.info(
      game.i18n.format('token-variants.notifications.info.caching-finished', {
        imageCount: Object.keys(CACHED_IMAGES).reduce((count, types) => count + CACHED_IMAGES[types].length, 0),
      })
    );

  if (staticCache && game.user.isGM) {
    saveCache(staticCacheFile);
  }
}

async function _readCacheFromFile(fileName) {
  CACHED_IMAGES = {};
  try {
    await jQuery.getJSON(fileName, (json) => {
      for (let category in json) {
        CACHED_IMAGES[category] = [];

        for (const img of json[category]) {
          if (Array.isArray(img)) {
            if (img.length === 3) {
              CACHED_IMAGES[category].push({ path: img[0], name: img[1], tags: img[2] });
            } else {
              CACHED_IMAGES[category].push({ path: img[0], name: img[1] });
            }
          } else {
            CACHED_IMAGES[category].push({ path: img, name: getFileName(img) });
          }
        }
      }
      if (!TVA_CONFIG.disableNotifs)
        ui.notifications.info(
          `Token Variant Art: Using Static Cache (${Object.keys(CACHED_IMAGES).reduce(
            (count, c) => count + CACHED_IMAGES[c].length,
            0
          )} images)`
        );
    });
  } catch (error) {
    ui.notifications.warn(`Token Variant Art: Static Cache not found`);
    CACHED_IMAGES = {};
    return false;
  }
  return true;
}
