import { existsSync, readdirSync, readFileSync } from 'fs';
import { Directories } from '@vizality/constants';
import { join } from 'path';

import Theme from '../entities/Theme';
import AddonManager from './Addon';

const typeOf = what => Array.isArray(what)
  ? 'array'
  : what === null
    ? 'null'
    : typeof what;

const listFormat = new Intl.ListFormat('en', { type: 'disjunction' });

const MANIFEST_VALIDATION = [
  {
    prop: 'name',
    types: [ 'string' ],
    type: 'required'
  },
  {
    prop: 'description',
    types: [ 'string' ],
    type: 'required'
  },
  {
    prop: 'version',
    types: [ 'string' ],
    type: 'required'
  },
  {
    prop: 'author',
    types: [ 'string', 'object' ],
    type: 'required'
  },
  {
    prop: 'theme',
    types: [ 'string' ],
    type: 'required'
  },
  {
    prop: 'license',
    types: [ 'string' ],
    type: 'optional'
  },
  {
    prop: 'discord',
    types: [ 'string' ],
    type: 'optional'
  }
];

export default class ThemeManager extends AddonManager {
  constructor (type, dir) {
    type = 'theme';
    dir = Directories.THEMES;
    super(type, dir);
  }

  async _getManifest (themeId) {
    try {
      /**
       * Check if it's a normal Vizality theme.
       */
      let manifest;
      let manifestFile = join(this.dir, themeId, 'manifest.json');
      if (existsSync(manifestFile)) {
        manifest = await import(manifestFile);
      } else {
        /**
         * Check if it's a Powercord theme.
         */
        manifestFile = join(this.dir, themeId, 'powercord_manifest.json');
        if (existsSync(manifestFile)) {
          manifest = await import(manifestFile);
        }

        /**
         * Let's check if it's a BetterDiscord theme.
         * Credits to creatable for most of this.
         * @see {@link https://github.com/Cr3atable/Powerconvert}
         * @author creatable
         * @license Apache-2.0
         */
        if (!manifest) {
          const themeDirectory = readdirSync(join(this.dir, themeId));
          const themeFile = themeDirectory?.filter(file => file.match(/\.(theme).((s?c)ss)$/))?.[0];
          if (themeFile) {
            const themeFileContents = readFileSync(join(this.dir, themeId, themeFile), 'utf8');
            const splitRegex = /[^\S\r\n]*?\r?\n[^\S\r\n]*?\*[^\S\r\n]?/;
            const escapedAtRegex = /^\\@/;

            function parseNewMeta (fileContent) {
              const block = fileContent?.split('/**', 2)?.[1]?.split('*/', 1)?.[0];
              const out = {};
              let field = '';
              let accum = '';
              for (const line of block?.split(splitRegex)) {
                if (line.length === 0) {
                  continue;
                }
                if (line.charAt(0) === '@' && line.charAt(1) !== ' ') {
                  out[field] = accum;
                  const l = line.indexOf(' ');
                  field = line.substr(1, l - 1);
                  accum = line.substr(l + 1);
                } else {
                  accum += ` ${line.replace('\\n', '\n').replace(escapedAtRegex, '@')}`;
                }
              }
              out[field] = accum.trim();
              delete out[''];
              return out;
            }

            function extractMeta (fileContent) {
              const firstLine = fileContent?.split('\n')?.[0];
              const hasNewMeta = firstLine?.includes('/**');
              if (hasNewMeta) {
                return parseNewMeta(fileContent);
              }
              throw new Error('META was not found.');
            }

            manifest = extractMeta(themeFileContents);
            manifest = {
              name: manifest.name,
              description: manifest.description,
              version: manifest.version,
              author: manifest.author,
              theme: themeFile
            };
          }
        }
      }

      if (!manifest) {
        throw new Error(`Failed to load manifest for "${themeId}"`);
      }

      return manifest;
    } catch (err) {
      return this._error(err);
    }
  }

  async mount (themeId) {
    const manifest = await this._getManifest(themeId);

    const errors = this._validateManifest(manifest);
    if (errors.length > 0) {
      return this._error(`Invalid manifest; Detected the following errors:\n\t${errors.join('\n\t')}`);
    }

    manifest.effectiveTheme = join(this.dir, themeId, manifest.theme);
    this._setAddonIcon(themeId, manifest);
    this._items.set(themeId, new Theme(themeId, manifest));
  }

  _validateManifest (manifest) {
    const errors = [];
    for (const entry of MANIFEST_VALIDATION) {
      const isValid = entry.types.some(type => typeOf(manifest[entry.prop]) === type);
      if (!isValid && entry.type === 'required') {
        errors.push(`Invalid ${entry.prop}: expexted a ${entry.types.length > 1 ? listFormat.format(entry.types) : entry.types[0]} but got ${typeOf(manifest[entry.prop])} instead.`);
      }
    }
    if (typeof manifest.theme !== 'string') {
      errors.push(`Invalid theme: expected a string but got ${typeOf(manifest.theme)}`);
    }
    const scssFileRegex = /\.((s?c)ss)$/;
    const matches = scssFileRegex.test(manifest.theme);
    if (!matches) {
      errors.push('Invalid theme: unsupported file extension');
    }
    return errors;
  }
}
