import fs from 'node:fs'
import path from 'node:path'
import type { Plugin } from 'vite'
import MagicString from 'magic-string'

export enum Languages {
    bg = 'bg',
    cs = 'cs',
    de = 'de',
    en_gb = 'en-gb',
    es = 'es',
    fr = 'fr',
    hu = 'hu',
    id = 'id',
    it = 'it',
    ja = 'ja',
    ko = 'ko',
    nl = 'nl',
    pl = 'pl',
    ps = 'ps',
    pt_br = 'pt-br',
    ru = 'ru',
    tr = 'tr',
    uk = 'uk',
    zh_hans = 'zh-hans',
    zh_hant = 'zh-hant',
}

export interface Options {
    locale: Languages;
    localeData?: Record<string, any>
}

/**
 * 在vite中dev模式下会使用esbuild对node_modules进行预编译，导致找不到映射表中的filepath，
 * 需要在预编译之前进行替换
 * @param options 替换语言包
 * @returns
 */
export function esbuildPluginMonacoEditorNls(options: Options): any {
    options = Object.assign({ locale: Languages.en_gb }, options)
    const CURRENT_LOCALE_DATA = getLocalizeMapping(options.locale, options.localeData)

    return {
        name: 'esbuild-plugin-monaco-editor-nls',
        setup(build) {
            build.onLoad({ filter: /esm[/\\]vs[/\\]nls\.js/ }, async () => {
                return {
                    contents: getLocalizeCode(CURRENT_LOCALE_DATA),
                    loader: 'js',
                }
            })

            build.onLoad(
                { filter: /monaco-editor[/\\]esm[/\\]vs.+\.js/ },
                async args => {
                    return {
                        contents: transformLocalizeFuncCode(
                            args.path,
                            CURRENT_LOCALE_DATA,
                        ),
                        loader: 'js',
                    }
                },
            )
        },
    }
}

/**
 * 使用了monaco-editor-nls的语言映射包，把原始localize(data, message)的方法，替换成了localize(path, data, defaultMessage)
 * vite build 模式下，使用rollup处理
 * @param options 替换语言包
 * @returns
 */
export default function (options: Options): Plugin {
    options = Object.assign({ locale: Languages.en_gb }, options)
    const CURRENT_LOCALE_DATA = getLocalizeMapping(options.locale, options.localeData)

    return {
        name: 'rollup-plugin-monaco-editor-nls',

        enforce: 'pre',

        load(filepath) {
            if (/esm[/\\]vs[/\\]nls\.js/.test(filepath)) {

                return getLocalizeCode(CURRENT_LOCALE_DATA)
            }
        },
        transform(code, filepath) {
            if (
                /monaco-editor[/\\]esm[/\\]vs.+\.js/.test(filepath)
                && !/esm[/\\]vs[/\\].*nls\.js/.test(filepath)
            ) {
                const re = /monaco-editor[/\\]esm[/\\](.+)(?=\.js)/
                if (re.exec(filepath) && code.includes('localize(')) {
                    let path = RegExp.$1
                    path = path.replaceAll('\\', '/')
                    code = code.replace(/localize\(/g, `localize('${path}', `)

                    return {
                        code: code,

                        /** 使用magic-string 生成 source map */
                        map: new MagicString(code).generateMap({
                            includeContent: true,
                            hires: true,
                            source: filepath,
                        }),
                    }
                }
            }
        },
    }
}

/**
 * 替换调用方法接口参数，替换成相应语言包语言
 * @param filepath 路径
 * @param CURRENT_LOCALE_DATA 替换规则
 * @returns
 */
function transformLocalizeFuncCode(
    filepath: string,
    _CURRENT_LOCALE_DATA: string,
) {
    let code = fs.readFileSync(filepath, 'utf8')
    const re = /monaco-editor[/\\]esm[/\\](.+)(?=\.js)/
    if (re.exec(filepath)) {
        let path = RegExp.$1
        path = path.replaceAll('\\', '/')
        code = code.replace(/localize\(/g, `localize('${path}', `)
    }

    return code
}

/**
 * 获取语言包
 * @param locale 语言
 * @param localeData
 * @returns
 */
function getLocalizeMapping(locale: Languages, localeData: Record<string, any> | undefined = undefined) {
    if (localeData) return JSON.stringify(localeData)
    const locale_data_path = path.join(__dirname, `./locale/${locale}.json`)

    return fs.readFileSync(locale_data_path) as unknown as string
}

/**
 * 替换代码
 * @param CURRENT_LOCALE_DATA 语言包
 * @returns
 */
function getLocalizeCode(CURRENT_LOCALE_DATA: string) {
    return `
    /* ---------------------------------------------------------------------------------------------
    *  Copyright (c) Microsoft Corporation. All rights reserved.
    *  Licensed under the MIT License. See License.txt in the project root for license information.
    *--------------------------------------------------------------------------------------------*/
   let isPseudo = typeof document !== 'undefined' && document.location && document.location.hash.indexOf('pseudo=true') >= 0
   const DEFAULT_TAG = 'i-default'
   function _format(message, args) {
       let result
       if (args.length === 0) {
           result = message
       } else {
           result = message.replace(/{(\\d+)}/g, (match, rest) => {
               const index = rest[0]
               const arg = args[index]
               let result = match
               if (typeof arg === 'string') {
                   result = arg
               } else if (typeof arg === 'number' || typeof arg === 'boolean' || arg === void 0 || arg === null) {
                   result = String(arg)
               }
   
               return result
           })
       }
       if (isPseudo) {
   
           // FF3B and FF3D is the Unicode zenkaku representation for [ and ]
           result = \`\uFF3B\${result.replace(/[aeiou]/g, '$&$&')}\uFF3D\`
       }
   
       return result
   }
   function findLanguageForModule(config, name) {
       let result = config[name]
       if (result) {
           return result
       }
       result = config['*']
       if (result) {
           return result
       }
   
       return null
   }
   function endWithSlash(path) {
       if (path.charAt(path.length - 1) === '/') {
           return path
       }
   
       return \`\${path}/\`
   }
   async function getMessagesFromTranslationsService(translationServiceUrl, language, name) {
       const url = \`\${endWithSlash(translationServiceUrl) + endWithSlash(language)}vscode/\${endWithSlash(name)}\`
       const res = await fetch(url)
       if (res.ok) {
           const messages = await res.json()
   
           return messages
       }
       throw new Error(\`\${res.status} - \${res.statusText}\`)
   }
   function createScopedLocalize(scope) {
       return function(idx, defaultValue) {
           const restArgs = Array.prototype.slice.call(arguments, 2)
   
           return _format(scope[idx], restArgs)
       }
   }
   function createScopedLocalize2(scope) {
       return (idx, defaultValue, ...args) => ({
           value: _format(scope[idx], args),
           original: _format(defaultValue, args),
       })
   }
   
   // export function localize(data, message, ...args) {
   //     return _format(message, args);
   // }

// ------------------------invoke----------------------------------------
    export function localize(path, data, defaultMessage, ...args) {
        var key = typeof data === 'object' ? data.key : data;
        var data = ${CURRENT_LOCALE_DATA} || {};
        var message = (data[path] || data?.contents?.[path] || {})[key];
        if (!message) {
            message = defaultMessage;
        }
        return _format(message, args);
    }
// ------------------------invoke----------------------------------------

   /**
    * @skipMangle
    */
   export function localize2(data, message, ...args) {
       const original = _format(message, args)
   
       return {
           value: original,
           original,
       }
   }
   
   /**
    * @skipMangle
    */
   export function getConfiguredDefaultLocale(_) {
   
       // This returns undefined because this implementation isn't used and is overwritten by the loader
       // when loaded.
       return undefined
   }
   
   /**
    * @skipMangle
    */
   export function setPseudoTranslation(value) {
       isPseudo = value
   }
   
   /**
    * Invoked in a built product at run-time
    * @skipMangle
    */
   export function create(key, data) {
       var _a
   
       return {
           localize: createScopedLocalize(data[key]),
           localize2: createScopedLocalize2(data[key]),
           getConfiguredDefaultLocale: (_a = data.getConfiguredDefaultLocale) !== null && _a !== void 0 ? _a : _ => undefined,
       }
   }
   
   /**
    * Invoked by the loader at run-time
    * @skipMangle
    */
   export function load(name, req, load, config) {
       var _a
       const pluginConfig = (_a = config['vs/nls']) !== null && _a !== void 0 ? _a : {}
       if (!name || name.length === 0) {
   
           // TODO: We need to give back the mangled names here
           return load({
               localize: localize,
               localize2: localize2,
               getConfiguredDefaultLocale: () => {
                   var _a
   
                   return (_a = pluginConfig.availableLanguages) === null || _a === void 0 ? void 0 : _a['*'] 
               },
           })
       }
       const language = pluginConfig.availableLanguages ? findLanguageForModule(pluginConfig.availableLanguages, name) : null
       const useDefaultLanguage = language === null || language === DEFAULT_TAG
       let suffix = '.nls'
       if (!useDefaultLanguage) {
           suffix = \`\${suffix}.\${language}\`
       }
       const messagesLoaded = messages => {
           if (Array.isArray(messages)) {
               messages.localize = createScopedLocalize(messages)
               messages.localize2 = createScopedLocalize2(messages)
           } else {
               messages.localize = createScopedLocalize(messages[name])
               messages.localize2 = createScopedLocalize2(messages[name])
           }
           messages.getConfiguredDefaultLocale = () => {
               var _a
   
               return (_a = pluginConfig.availableLanguages) === null || _a === void 0 ? void 0 : _a['*'] 
           }
           load(messages)
       }
       if (typeof pluginConfig.loadBundle === 'function') {
           pluginConfig.loadBundle(name, language, (err, messages) => {
   
               // We have an error. Load the English default strings to not fail
               if (err) {
                   req([\`\${name}.nls\`], messagesLoaded)
               } else {
                   messagesLoaded(messages)
               }
           })
       } else if (pluginConfig.translationServiceUrl && !useDefaultLanguage) {
           (async() => {
               var _a
               try {
                   const messages = await getMessagesFromTranslationsService(pluginConfig.translationServiceUrl, language, name)
   
                   return messagesLoaded(messages)
               } catch (err) {
   
                   // Language is already as generic as it gets, so require default messages
                   if (!language.includes('-')) {
                       console.error(err)
   
                       return req([\`\${name}.nls\`], messagesLoaded)
                   }
                   try {
   
                       // Since there is a dash, the language configured is a specific sub-language of the same generic language.
                       // Since we were unable to load the specific language, try to load the generic language. Ex. we failed to find a
                       // Swiss German (de-CH), so try to load the generic German (de) messages instead.
                       const genericLanguage = language.split('-')[0]
                       const messages = await getMessagesFromTranslationsService(pluginConfig.translationServiceUrl, genericLanguage, name);
   
                       // We got some messages, so we configure the configuration to use the generic language for this session.
                       (_a = pluginConfig.availableLanguages) !== null && _a !== void 0 ? _a : pluginConfig.availableLanguages = {}
                       pluginConfig.availableLanguages['*'] = genericLanguage
   
                       return messagesLoaded(messages)
                   } catch (err) {
                       console.error(err)
   
                       return req([\`\${name}.nls\`], messagesLoaded)
                   }
               }
           })()
       } else {
           req([name + suffix], messagesLoaded, err => {
               if (suffix === '.nls') {
                   console.error('Failed trying to load default language strings', err)
   
                   return
               }
               console.error(\`Failed to load message bundle for language \${language}. Falling back to the default language:\`, err)
               req([\`\${name}.nls\`], messagesLoaded)
           })
       }
   }  
    `
}
