/*
 * notion-enhancer core: api
 * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
 * (https://notion-enhancer.github.io/) under the MIT license
 */

'use strict';

import { fmt, registry } from './_.mjs';

const check = async (
  mod,
  key,
  value,
  types,
  {
    extension = '',
    error = `invalid ${key} (${extension ? `${extension} ` : ''}${types}): ${JSON.stringify(
      value
    )}`,
    optional = false,
  } = {}
) => {
  let test;
  for (const type of Array.isArray(types) ? [types] : types.split('|')) {
    if (type === 'file') {
      test =
        value && !value.startsWith('http')
          ? await fmt.is(`repo/${mod._dir}/${value}`, type, { extension })
          : false;
    } else test = await fmt.is(value, type, { extension });
    if (test) break;
  }
  if (!test) {
    if (optional && (await fmt.is(value, 'undefined'))) return true;
    if (error) mod._err(error);
    return false;
  }
  return true;
};

const validateEnvironments = async (mod) => {
    mod.environments = mod.environments ?? registry.supportedEnvs;
    const isArray = await check(mod, 'environments', mod.environments, 'array');
    if (!isArray) return false;
    return mod.environments.map((tag) =>
      check(mod, 'environments.env', tag, registry.supportedEnvs)
    );
  },
  validateTags = async (mod) => {
    const isArray = await check(mod, 'tags', mod.tags, 'array');
    if (!isArray) return false;
    const categoryTags = ['core', 'extension', 'theme'],
      containsCategory = mod.tags.filter((tag) => categoryTags.includes(tag)).length;
    if (!containsCategory) {
      mod._err(
        `invalid tags (must contain at least one of 'core', 'extension', or 'theme'):
        ${JSON.stringify(mod.tags)}`
      );
      return false;
    }
    const isTheme = mod.tags.includes('theme'),
      hasThemeMode = mod.tags.includes('light') || mod.tags.includes('dark'),
      isBothThemeModes = mod.tags.includes('light') && mod.tags.includes('dark');
    if (isTheme && (!hasThemeMode || isBothThemeModes)) {
      mod._err(
        `invalid tags (themes must be either 'light' or 'dark', not neither or both):
        ${JSON.stringify(mod.tags)}`
      );
      return false;
    }
    return mod.tags.map((tag) => check(mod, 'tags.tag', tag, 'string'));
  },
  validateAuthors = async (mod) => {
    const isArray = await check(mod, 'authors', mod.authors, 'array');
    if (!isArray) return false;
    return mod.authors.map((author) => [
      check(mod, 'authors.author.name', author.name, 'string'),
      check(mod, 'authors.author.email', author.email, 'email', { optional: true }),
      check(mod, 'authors.author.homepage', author.homepage, 'url'),
      check(mod, 'authors.author.avatar', author.avatar, 'url'),
    ]);
  },
  validateCSS = async (mod) => {
    const isArray = await check(mod, 'css', mod.css, 'object');
    if (!isArray) return false;
    const tests = [];
    for (let dest of ['frame', 'client', 'menu']) {
      if (!mod.css[dest]) continue;
      let test = await check(mod, `css.${dest}`, mod.css[dest], 'array');
      if (test) {
        test = mod.css[dest].map((file) =>
          check(mod, `css.${dest}.file`, file, 'file', { extension: '.css' })
        );
      }
      tests.push(test);
    }
    return tests;
  },
  validateJS = async (mod) => {
    const isArray = await check(mod, 'js', mod.js, 'object');
    if (!isArray) return false;
    const tests = [];
    for (let dest of ['frame', 'client', 'menu']) {
      if (!mod.js[dest]) continue;
      let test = await check(mod, `js.${dest}`, mod.js[dest], 'array');
      if (test) {
        test = mod.js[dest].map((file) =>
          check(mod, `js.${dest}.file`, file, 'file', { extension: '.mjs' })
        );
      }
      tests.push(test);
    }
    if (mod.js.electron) {
      const isArray = await check(mod, 'js.electron', mod.js.electron, 'array');
      if (isArray) {
        for (const file of mod.js.electron) {
          const isObject = await check(mod, 'js.electron.file', file, 'object');
          if (!isObject) {
            tests.push(false);
            continue;
          }
          tests.push([
            check(mod, 'js.electron.file.source', file.source, 'file', {
              extension: '.mjs',
            }),
            // referencing the file within the electron app
            // existence can't be validated, so only format is
            check(mod, 'js.electron.file.target', file.target, 'string', {
              extension: '.js',
            }),
          ]);
        }
      } else tests.push(false);
    }
    return tests;
  },
  validateOptions = async (mod) => {
    const isArray = await check(mod, 'options', mod.options, 'array');
    if (!isArray) return false;
    const tests = [];
    for (const option of mod.options) {
      const key = 'options.option',
        optTypeValid = await check(mod, `${key}.type`, option.type, registry.optionTypes);
      if (!optTypeValid) {
        tests.push(false);
        continue;
      }
      option.environments = option.environments ?? registry.supportedEnvs;
      tests.push([
        check(mod, `${key}.key`, option.key, 'alphanumeric'),
        check(mod, `${key}.label`, option.label, 'string'),
        check(mod, `${key}.tooltip`, option.tooltip, 'string', {
          optional: true,
        }),
        check(mod, `${key}.environments`, option.environments, 'array').then((isArray) => {
          if (!isArray) return false;
          return option.environments.map((environment) =>
            check(mod, `${key}.environments.env`, environment, registry.supportedEnvs)
          );
        }),
      ]);
      switch (option.type) {
        case 'toggle':
          tests.push(check(mod, `${key}.value`, option.value, 'boolean'));
          break;
        case 'select': {
          let test = await check(mod, `${key}.values`, option.values, 'array');
          if (test) {
            test = option.values.map((value) =>
              check(mod, `${key}.values.value`, value, 'string')
            );
          }
          tests.push(test);
          break;
        }
        case 'text':
        case 'hotkey':
          tests.push(check(mod, `${key}.value`, option.value, 'string'));
          break;
        case 'number':
        case 'color':
          tests.push(check(mod, `${key}.value`, option.value, option.type));
          break;
        case 'file': {
          let test = await check(mod, `${key}.extensions`, option.extensions, 'array');
          if (test) {
            test = option.extensions.map((ext) =>
              check(mod, `${key}.extensions.extension`, ext, 'string')
            );
          }
          tests.push(test);
          break;
        }
      }
    }
    return tests;
  };

/**
 * internally used to validate mod.json files and provide helpful errors
 * @private
 * @param {object} mod - a mod's mod.json in object form
 * @returns {boolean} whether or not the mod has passed validation
 */
export async function validate(mod) {
  let conditions = [
    check(mod, 'name', mod.name, 'string'),
    check(mod, 'id', mod.id, 'uuid'),
    check(mod, 'version', mod.version, 'semver'),
    validateEnvironments(mod),
    check(mod, 'description', mod.description, 'string'),
    check(mod, 'preview', mod.preview, 'file|url', { optional: true }),
    validateTags(mod),
    validateAuthors(mod),
    validateCSS(mod),
    validateJS(mod),
    validateOptions(mod),
  ];
  do {
    conditions = await Promise.all(conditions.flat(Infinity));
  } while (conditions.some((condition) => Array.isArray(condition)));
  return conditions.every((passed) => passed);
}