import { Autowired, Injectable } from '@opensumi/di';
import {
  IPreferenceSettingsService,
  Logger,
  PreferenceSchemaProvider,
  PreferenceService,
} from '@opensumi/ide-core-browser';
import {
  AppLifeCycleServiceToken,
  ContributionProvider,
  Deferred,
  DisposableCollection,
  Emitter,
  Event,
  ExtensionDidContributes,
  GeneralSettingsId,
  IAppLifeCycleService,
  IThemeColor,
  LifeCyclePhase,
  OnEvent,
  PreferenceScope,
  URI,
  WithEventBus,
  isFunction,
  isLinux,
  isObject,
  isWindows,
  localize,
  uuid,
} from '@opensumi/ide-core-common';

import { ICSSStyleService, ThemeContributionProvider } from '../common';
import { Color } from '../common/color';
import {
  editorSelectionBackground,
  getColorRegistry,
  ktInputSelectionBackground,
  selectionBackground,
} from '../common/color-registry';
import { CSSVarRegistry } from '../common/css-var';
import { ThemeChangedEvent } from '../common/event';
import {
  ColorIdentifier,
  DEFAULT_THEME_ID,
  ExtColorContribution,
  IColorCustomizations,
  IColorMap,
  ITheme,
  IThemeContribution,
  IThemeData,
  IThemeService,
  IThemeStore,
  ITokenColorCustomizations,
  ITokenColorizationRule,
  ThemeInfo,
  ThemeType,
  colorIdPattern,
  getBuiltinRules,
  getThemeId,
  getThemeType,
  getThemeTypeSelector,
} from '../common/theme.service';

import { ThemeData } from './theme-data';
import { ThemeStore } from './theme-store';

export const CUSTOM_WORKBENCH_COLORS_SETTING = 'workbench.colorCustomizations';
export const CUSTOM_EDITOR_COLORS_SETTING = 'editor.tokenColorCustomizations';
export const COLOR_THEME_SETTING = 'general.theme';

const tokenGroupToScopesMap = {
  comments: ['comment', 'punctuation.definition.comment'],
  strings: ['string', 'meta.embedded.assembly'],
  keywords: ['keyword - keyword.operator', 'keyword.control', 'storage', 'storage.type'],
  numbers: ['constant.numeric'],
  types: ['entity.name.type', 'entity.name.class', 'support.type', 'support.class'],
  functions: ['entity.name.function', 'support.function'],
  variables: ['variable', 'entity.name.variable'],
};

@Injectable()
export class WorkbenchThemeService extends WithEventBus implements IThemeService {
  private colorRegistry = getColorRegistry();

  private cssVarRegistry = CSSVarRegistry.instance();

  private colorClassNameMap = new Map<string, string>();

  colorThemeLoaded: Deferred<void> = new Deferred();

  public currentThemeId: string;
  private currentTheme?: Theme;
  private latestApplyTheme: string;

  private themes: Map<string, ThemeData> = new Map();
  private themeContributionRegistry: Map<string, { contribution: IThemeContribution; basePath: URI }> = new Map();

  private themeChangeEmitter: Emitter<ITheme> = new Emitter();
  protected extensionReady: boolean;

  public onThemeChange: Event<ITheme> = this.themeChangeEmitter.event;

  @Autowired(IThemeStore)
  private themeStore: ThemeStore;

  @Autowired()
  private logger: Logger;

  @Autowired(PreferenceService)
  private preferenceService: PreferenceService;

  @Autowired(PreferenceSchemaProvider)
  private preferenceSchemaProvider: PreferenceSchemaProvider;

  @Autowired(IPreferenceSettingsService)
  private preferenceSettings: IPreferenceSettingsService;

  @Autowired(ICSSStyleService)
  private readonly styleService: ICSSStyleService;

  @Autowired(ThemeContributionProvider)
  protected readonly themeContributionProvider: ContributionProvider<ThemeContributionProvider>;

  @Autowired(AppLifeCycleServiceToken)
  private readonly appLifeCycleService: IAppLifeCycleService;

  constructor() {
    super();
    this.listen();
    this.applyPlatformClass();
  }

  async ensureValidTheme(defaultThemeId = DEFAULT_THEME_ID): Promise<string> {
    await Event.toPromise(
      Event.filter(this.appLifeCycleService.onDidLifeCyclePhaseChange, (phase) => phase === LifeCyclePhase.Ready),
    );

    const themeInfos = this.getAvailableThemeInfos();
    // scope 优先级从高到低匹配有效主题
    const scopes = PreferenceScope.getReversedScopes();

    let validThemeId = '';

    for (const scope of scopes) {
      const provider = this.preferenceService.getProvider(scope);
      const scopeThemeId = provider!.get<string>(GeneralSettingsId.Theme);

      if (themeInfos.some((info) => info.themeId === scopeThemeId)) {
        validThemeId = scopeThemeId!;
        break;
      }
    }

    // 做最后兜底
    if (!validThemeId) {
      return defaultThemeId;
    }

    return validThemeId;
  }

  @OnEvent(ExtensionDidContributes)
  async onDidExtensionContributes() {
    const themeMap = this.getAvailableThemeInfos().reduce((pre: Map<string, string>, cur: ThemeInfo) => {
      if (!pre.has(cur.themeId)) {
        pre.set(cur.themeId, cur.name);
      }
      return pre;
    }, new Map());

    this.preferenceSettings.setEnumLabels(COLOR_THEME_SETTING, Object.fromEntries(themeMap.entries()));
    if (!this.currentThemeId && !this.currentTheme) {
      this.colorThemeLoaded.resolve();
    }
  }

  get preferenceThemeId(): string | undefined {
    return this.preferenceService.get<string>(COLOR_THEME_SETTING);
  }

  public registerThemes(themeContributions: IThemeContribution[], extPath: URI) {
    const disposables = new DisposableCollection();
    disposables.push({
      dispose: () => this.doSetPreferenceSchema(),
    });

    themeContributions.forEach(async (contribution) => {
      const themeExtContribution = { basePath: extPath, contribution };
      const themeId = getThemeId(contribution);

      this.themeContributionRegistry.set(themeId, themeExtContribution);

      if (this.preferenceThemeId === themeId) {
        await this.applyTheme(this.preferenceThemeId);
      }

      disposables.push({
        dispose: () => {
          this.themeContributionRegistry.delete(themeId);
        },
      });

      disposables.push({
        dispose: () => {
          if (this.currentThemeId === themeId) {
            this.applyTheme(DEFAULT_THEME_ID);
          }
        },
      });
    });

    this.doSetPreferenceSchema();

    return disposables;
  }

  public async applyTheme(themeId: string) {
    if (this.currentThemeId === themeId) {
      return;
    }

    const prevThemeType = this.currentTheme ? this.currentTheme.type : 'dark';
    this.currentThemeId = themeId;
    /**
     * 这里 `applyTheme` 默认应该按照最后一个应用的主题进行加载
     * 但由于 `getTheme` 存在时序问题，例如：
     * 主题 A，E，分别由插件 A，E 贡献
     * 这里先调用 applyTheme(E), 再调用 applyTheme(A)
     * 旧的逻辑由于插件 A ... E 的加载顺序问题，会存在 A 比 E 快加载的情况导致最终应用了错误的主题
     *
     * 故这里增加额外判断，保障最后一个加载的主题应用
     */
    this.latestApplyTheme = themeId;
    const theme = await this.getTheme(themeId);
    if (this.latestApplyTheme !== themeId) {
      return;
    }
    const themeType = getThemeType(theme.base);
    this.currentTheme = new Theme(themeType, theme);
    this.currentTheme.setCustomColors(this.colorCustomizations);
    this.currentTheme.setCustomTokenColors(this.tokenColorCustomizations);

    const currentThemeType = this.currentTheme.type;

    this.toggleBaseThemeClass(prevThemeType, currentThemeType);
    this.doApplyTheme(this.currentTheme);

    this.colorThemeLoaded.resolve();
  }

  public registerColor(contribution: ExtColorContribution) {
    if (!this.checkColorContribution(contribution)) {
      return;
    }
    const { defaults } = contribution;
    this.colorRegistry.registerColor(
      contribution.id,
      {
        light: this.parseColorValue(defaults.light, 'configuration.colors.defaults.light'),
        dark: this.parseColorValue(defaults.dark, 'configuration.colors.defaults.dark'),
        hcDark: this.parseColorValue(
          defaults.highContrast ?? defaults.dark,
          'configuration.colors.defaults.highContrast',
        ),
        hcLight: this.parseColorValue(
          defaults.highContrastLight ?? defaults.light,
          'configuration.colors.defaults.highContrastLight',
        ),
      },
      contribution.description,
    );
  }

  // @deprecated 请直接使用sync方法，主题加载由时序保障
  public async getCurrentTheme() {
    if (this.currentTheme) {
      return this.currentTheme;
    } else {
      const themeData = await this.getTheme(this.currentThemeId);
      return new Theme(getThemeType(themeData.base), themeData);
    }
  }

  public getCurrentThemeSync() {
    if (this.currentTheme) {
      return this.currentTheme;
    } else {
      const themeData = this.themeStore.loadDefaultTheme();
      return new Theme(getThemeType(themeData.base), themeData);
    }
  }

  public getColor(colorId: string | IThemeColor | undefined): string | undefined {
    if (!colorId) {
      return undefined;
    }
    if (typeof colorId === 'string') {
      return colorId;
    }
    const color = this.currentTheme?.getColor(colorId.id);
    return color ? Color.Format.CSS.formatHexA(color) : '';
  }

  // 将 colorId 转换成 css 变量
  public getColorVar(colorId: string | IThemeColor | undefined): string | undefined {
    if (!colorId) {
      return undefined;
    }
    if (typeof colorId === 'string') {
      return colorId;
    }
    const colorKey = colorId.id;
    return colorKey ? `var(--${colorKey.replace(/\./g, '-')})` : undefined;
  }

  public getColorClassNameByColorToken(colorId: string | IThemeColor): string | undefined {
    if (!colorId) {
      return undefined;
    }
    const id = typeof colorId === 'string' ? colorId : colorId.id;
    if (this.colorClassNameMap.has(id)) {
      return this.colorClassNameMap.get(id)!;
    }
    const className = `color-token-${uuid()}`;
    this.styleService.addClass(className, {
      color: this.getColorVar({ id })!,
    });
    this.colorClassNameMap.set(id, className);
    return className;
  }

  public getAvailableThemeInfos(): ThemeInfo[] {
    const themeInfos: ThemeInfo[] = [];
    for (const { contribution } of this.themeContributionRegistry.values()) {
      const { label, uiTheme, extensionId } = contribution;
      themeInfos.push({
        themeId: getThemeId(contribution),
        name: label,
        base: uiTheme || 'vs',
        extensionId,
      });
    }
    return themeInfos;
  }

  protected doSetPreferenceSchema() {
    const enums = this.getAvailableThemeInfos().map((info) => info.themeId);
    this.preferenceSchemaProvider.setSchema(
      {
        properties: {
          [COLOR_THEME_SETTING]: {
            type: 'string',
            default: enums[0],
            enum: enums,
          },
        },
      },
      true,
    );
  }

  private get colorCustomizations(): IColorCustomizations {
    return this.preferenceService.getValid(CUSTOM_WORKBENCH_COLORS_SETTING, {});
  }

  private get tokenColorCustomizations(): ITokenColorCustomizations {
    return this.preferenceService.getValid<ITokenColorCustomizations>(CUSTOM_EDITOR_COLORS_SETTING, {});
  }

  private listen() {
    this.addDispose(
      this.eventBus.on(ThemeChangedEvent, (e) => {
        this.themeChangeEmitter.fire(e.payload.theme);
      }),
    );

    this.addDispose(
      Event.debounce(
        this.preferenceService.onPreferenceChanged,
        (_, e) => e,
        50,
      )(async (e) => {
        if (e.preferenceName === COLOR_THEME_SETTING) {
          // make sure the theme is registered
          if (this.themeContributionRegistry.has(e.newValue)) {
            await this.applyTheme(e.newValue);
          }
        }

        if (this.currentTheme) {
          switch (e.preferenceName) {
            case CUSTOM_WORKBENCH_COLORS_SETTING: {
              this.currentTheme.setCustomColors(e.newValue);
              this.doApplyTheme(this.currentTheme);
              break;
            }
            case CUSTOM_EDITOR_COLORS_SETTING: {
              this.currentTheme.setCustomTokenColors(e.newValue);
              this.eventBus.fire(
                new ThemeChangedEvent({
                  theme: this.currentTheme,
                }),
              );
              break;
            }
            default:
              break;
          }
        }
      }),
    );

    this.addDispose(
      this.colorRegistry.onDidColorChangedEvent((e) => {
        if (this.currentTheme) {
          this.doApplyTheme.apply(this, [this.currentTheme]);
        }
      }),
    );
  }

  private checkColorContribution(contribution: ExtColorContribution) {
    if (typeof contribution.id !== 'string' || contribution.id.length === 0) {
      this.logger.error(localize('invalid.id', "'configuration.colors.id' must be defined and can not be empty"));
      return false;
    }
    if (!contribution.id.match(colorIdPattern)) {
      this.logger.error(localize('invalid.id.format', "'configuration.colors.id' must follow the word[.word]*"));
      return false;
    }
    if (typeof contribution.description !== 'string' || contribution.id.length === 0) {
      this.logger.error(
        localize('invalid.description', "'configuration.colors.description' must be defined and can not be empty"),
      );
      return false;
    }
    const defaults = contribution.defaults;
    if (
      !defaults ||
      typeof defaults !== 'object' ||
      typeof defaults.light !== 'string' ||
      typeof defaults.dark !== 'string' ||
      typeof defaults.highContrast !== 'string'
    ) {
      this.logger.error(
        localize(
          'invalid.defaults',
          "'configuration.colors.defaults' must be defined and must contain 'light', 'dark' and 'highContrast'",
        ),
      );
      return false;
    }
    return true;
  }

  private parseColorValue = (s: string, name: string) => {
    if (s.length > 0) {
      if (s[0] === '#') {
        return Color.Format.CSS.parseHex(s);
      } else {
        return s;
      }
    }
    this.logger.error(
      localize(
        'invalid.default.colorType',
        '{0} must be either a color value in hex (#RRGGBB[AA] or #RGB[A]) or the identifier of a themable color which provides the default.',
        name,
      ),
    );
    return Color.red;
  };

  private async getTheme(id: string): Promise<IThemeData> {
    const theme = this.themes.get(id);
    if (theme) {
      return theme;
    }
    const themeInfo = this.themeContributionRegistry.get(id);
    if (themeInfo) {
      const { contribution, basePath } = themeInfo;
      return await this.themeStore.getThemeData(contribution, basePath);
    }
    return await this.themeStore.getThemeData();
  }

  private doApplyTheme(theme: Theme) {
    const registeredColors = this.colorRegistry.getColors();
    const colors = {} as Record<string, string | undefined>;

    registeredColors.forEach((c) => {
      const colorId = c.id;
      const color = theme.getColor(colorId);
      colors[colorId] = color ? color.toString() : '';
    });

    const contributions = this.themeContributionProvider.getContributions();
    for (const c of contributions) {
      if (isFunction(c.onWillApplyTheme)) {
        const contributionColor = c.onWillApplyTheme(theme);
        Object.assign(colors, contributionColor);
      }
    }

    // fallback to editorSelectionBackground when global selectionBackgroun is null.
    if (!theme.getColor(selectionBackground)) {
      colors[selectionBackground] = theme.getColor(editorSelectionBackground)?.toString();
      colors[ktInputSelectionBackground] = colors[selectionBackground];
    }

    let cssVariables = ':root{';
    let editorCssVariables = '#workbench-editor .monaco-editor {';
    for (const colorKey of Object.keys(colors)) {
      const targetColor = colors[colorKey] || theme.getColor(colorKey);
      if (targetColor) {
        const hexRule = `--${colorKey.replace(/\./g, '-')}: ${targetColor.toString()};\n`;
        if (colorKey.startsWith('vscode')) {
          editorCssVariables += hexRule;
        } else {
          cssVariables += hexRule;
        }
      }
    }

    this.cssVarRegistry.getVars().forEach((value, key) => {
      cssVariables += `--${key}: ${value};\n`;
    });

    let styleNode = document.getElementById('theme-style');
    if (styleNode) {
      styleNode.innerHTML = editorCssVariables + '}' + cssVariables + '}';
    } else {
      styleNode = document.createElement('style');
      styleNode.id = 'theme-style';
      styleNode.innerHTML = editorCssVariables + '}' + cssVariables + '}';
      document.getElementsByTagName('head')[0].appendChild(styleNode);
    }
    if (this.currentTheme) {
      this.eventBus.fire(
        new ThemeChangedEvent({
          theme: this.currentTheme,
        }),
      );
    }
  }

  protected toggleBaseThemeClass(prevThemeType: ThemeType, themeType: ThemeType) {
    const htmlNode = document.getElementsByTagName('html')[0];
    htmlNode.classList.remove(getThemeTypeSelector(prevThemeType));
    htmlNode.classList.add(getThemeTypeSelector(themeType));
  }

  protected applyPlatformClass() {
    const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac';
    const rootNode = document.getElementsByTagName('body')[0]!;
    rootNode.classList.add(platformClass);
  }
}

export class Themable extends WithEventBus {
  @Autowired(IThemeService)
  themeService: WorkbenchThemeService;

  protected theme: ITheme;

  constructor() {
    super();
    this.listenThemeChange();
  }

  get currentTheme() {
    return this.theme;
  }

  // 传入色值ID，主题色未定义时，若useDefault为false则返回undefined
  protected async getColor(id: ColorIdentifier, useDefault?: boolean) {
    if (!this.theme) {
      this.theme = await this.themeService.getCurrentTheme();
    }
    return this.theme.getColor(id, useDefault);
  }

  protected getColorSync(id: ColorIdentifier, useDefault?: boolean) {
    if (!this.theme) {
      this.theme = this.themeService.getCurrentThemeSync();
    }
    return this.theme.getColor(id, useDefault);
  }

  private listenThemeChange() {
    this.themeService.onThemeChange((theme) => {
      this.theme = theme;
      this.style();
    });
  }

  // themeChange时自动调用，首次调用时机需要模块自己判断
  async style(): Promise<void> {
    // use theme
  }
}

class Theme implements ITheme {
  readonly type: ThemeType;
  readonly themeData: IThemeData;
  private readonly colorRegistry = getColorRegistry();
  private readonly defaultColors: { [colorId: string]: Color | undefined } = Object.create(null);

  private colorMap: IColorMap;
  private customColorMap: IColorMap = {};
  private customTokenColors: ITokenColorizationRule[] = [];

  constructor(type: ThemeType, themeData: IThemeData) {
    this.type = type;
    this.themeData = themeData;
    this.patchColors();
    this.patchTokenColors();
    this.themeData.loadCustomTokens(this.customTokenColors);
  }

  getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined {
    const color = this.customColorMap[colorId] || this.getColors()[colorId];
    if (color) {
      return color;
    }
    if (useDefault !== false) {
      return this.getDefault(colorId);
    }
    return undefined;
  }

  defines(color: ColorIdentifier): boolean {
    if (this.customColorMap[color] || this.themeData.colors[color]) {
      return true;
    }
    return false;
  }

  setCustomColors(colors: IColorCustomizations) {
    this.customColorMap = {};
    this.overwriteCustomColors(colors);

    const themeSpecificColors = colors[`[${this.themeData.name}]`] as IColorCustomizations;
    if (isObject(themeSpecificColors)) {
      this.overwriteCustomColors(themeSpecificColors);
    }
  }

  setCustomTokenColors(customTokenColors: ITokenColorCustomizations) {
    this.customTokenColors = [];

    // first add the non-theme specific settings
    this.addCustomTokenColors(customTokenColors);

    // append theme specific settings. Last rules will win.
    const themeSpecificTokenColors = customTokenColors[`[${this.themeData.name}]`] as ITokenColorCustomizations;
    if (isObject(themeSpecificTokenColors)) {
      this.addCustomTokenColors(themeSpecificTokenColors);
    }
    this.themeData.loadCustomTokens(this.customTokenColors);
  }

  private addCustomTokenColors(customTokenColors: ITokenColorCustomizations) {
    // Put the general customizations such as comments, strings, etc. first so that
    // they can be overridden by specific customizations like "string.interpolated"
    // eslint-disable-next-line guard-for-in
    for (const tokenGroup in tokenGroupToScopesMap) {
      const group = tokenGroup as keyof typeof tokenGroupToScopesMap; // TS doesn't type 'tokenGroup' properly
      const value = customTokenColors[group];
      if (value) {
        const settings = typeof value === 'string' ? { foreground: value } : value;
        const scopes = tokenGroupToScopesMap[group];
        for (const scope of scopes) {
          this.customTokenColors.push({ scope, settings });
        }
      }
    }

    // specific customizations
    if (Array.isArray(customTokenColors.textMateRules)) {
      for (const rule of customTokenColors.textMateRules) {
        if (rule.scope && rule.settings) {
          this.customTokenColors.push(rule);
        }
      }
    }
  }

  private overwriteCustomColors(colors: IColorCustomizations) {
    // eslint-disable-next-line guard-for-in
    for (const id in colors) {
      const colorVal = colors[id];
      if (typeof colorVal === 'string') {
        this.customColorMap[id] = Color.fromHex(colorVal);
      }
    }
  }

  protected patchColors() {
    const colorContributions = this.colorRegistry.getColors();
    for (const colorContribution of colorContributions) {
      const id = colorContribution.id;
      const colorMap = this.themeData.colorMap;
      if (!colorMap[id]) {
        const color = this.colorRegistry.resolveDefaultColor(id, this);
        if (color) {
          colorMap[id] = color;
          this.themeData.colors[id] = Color.Format.CSS.formatHexA(color);
        }
      }
    }
  }

  // 将encodedTokensColors转为monaco可用的形式
  private patchTokenColors() {
    // 当默认颜色不在settings当中时，需要补充至颜色值中，默认颜色设置 scope 为 ['']
    // 需要注意的是，后续进行取值时，会依赖数组顺序，初始化后，请不要随意修改
    if (
      this.themeData.themeSettings.filter((setting) => setting.scope?.length === 1 && setting.scope[0] === '')
        .length === 0
    ) {
      this.themeData.themeSettings.unshift({
        settings: {
          foreground: this.themeData.colors['editor.foreground']
            ? this.themeData.colors['editor.foreground'].substring(0, 7)
            : Color.Format.CSS.formatHexA(this.colorRegistry.resolveDefaultColor('editor.foreground', this)!), // 这里要去掉透明度信息
          background: this.themeData.colors['editor.background']
            ? this.themeData.colors['editor.background'].substring(0, 7)
            : Color.Format.CSS.formatHexA(this.colorRegistry.resolveDefaultColor('editor.background', this)!),
        },
        scope: [''],
      });
    }
  }

  // 返回主题内的颜色值
  private getColors(): IColorMap {
    if (!this.colorMap) {
      const colorMap = Object.create(null);
      // eslint-disable-next-line guard-for-in
      for (const id in this.themeData.colorMap) {
        colorMap[id] = this.themeData.colorMap[id];
      }
      if (this.themeData.inherit) {
        const baseData = getBuiltinRules(this.themeData.base);
        for (const id in baseData.colors) {
          if (!colorMap[id]) {
            colorMap[id] = Color.fromHex(baseData.colors[id]);
          }
        }
      }
      this.colorMap = colorMap;
    }
    return this.colorMap;
  }

  private getDefault(colorId: ColorIdentifier): Color | undefined {
    let color = this.defaultColors[colorId];
    if (color) {
      return color;
    }
    color = this.colorRegistry.resolveDefaultColor(colorId, this);
    this.defaultColors[colorId] = color;
    return color;
  }
}
