import { Injectable } from '@opensumi/di';
import { IJSONSchema, IJSONSchemaMap } from '@opensumi/ide-core-common/lib/json-schema';
import { localize } from '@opensumi/ide-core-common/lib/localize';

import {
  ISemanticTokenRegistry,
  SemanticTokenDefaultRule,
  TOKEN_TYPE_WILDCARD,
  TokenSelector,
  TokenStyleDefaults,
  TokenTypeOrModifierContribution,
  fontStylePattern,
  getStylingSchemeEntry,
  parseClassifierString,
  selectorPattern,
  typeAndModifierIdPattern,
} from '../common/semantic-tokens-registry';

@Injectable()
export class SemanticTokenRegistryImpl implements ISemanticTokenRegistry {
  private typeHierarchy: { [id: string]: string[] } = Object.create(null);

  private tokenTypeById: { [key: string]: TokenTypeOrModifierContribution } = Object.create(null);
  private tokenModifierById: { [key: string]: TokenTypeOrModifierContribution } = Object.create(null);

  private currentTypeNumber = 0;
  private currentModifierBit = 1;

  private tokenStylingDefaultRules: SemanticTokenDefaultRule[] = [];

  private tokenStylingSchema: IJSONSchema & {
    properties: IJSONSchemaMap;
    patternProperties: IJSONSchemaMap;
  } = {
    type: 'object',
    properties: {},
    patternProperties: {
      [selectorPattern]: getStylingSchemeEntry(),
    },
    additionalProperties: false,
    definitions: {
      style: {
        type: 'object',
        description: localize('schema.token.settings', 'Colors and styles for the token.'),
        properties: {
          foreground: {
            type: 'string',
            description: localize('schema.token.foreground', 'Foreground color for the token.'),
            format: 'color-hex',
            default: '#ff0000',
          },
          background: {
            type: 'string',
            deprecationMessage: localize(
              'schema.token.background.warning',
              'Token background colors are currently not supported.',
            ),
          },
          fontStyle: {
            type: 'string',
            description: localize(
              'schema.token.fontStyle',
              "Sets the all font styles of the rule: 'italic', 'bold' or 'underline' or a combination. All styles that are not listed are unset. The empty string unsets all styles.",
            ),
            pattern: fontStylePattern,
            patternErrorMessage: localize(
              'schema.fontStyle.error',
              "Font style must be 'italic', 'bold' or 'underline' or a combination. The empty string unsets all styles.",
            ),
            defaultSnippets: [
              {
                label: localize('schema.token.fontStyle.none', 'None (clear inherited style)'),
                bodyText: '""',
              },
              { body: 'italic' },
              { body: 'bold' },
              { body: 'underline' },
              { body: 'italic underline' },
              { body: 'bold underline' },
              { body: 'italic bold underline' },
            ],
          },
          bold: {
            type: 'boolean',
            description: localize(
              'schema.token.bold',
              "Sets or unsets the font style to bold. Note, the presence of 'fontStyle' overrides this setting.",
            ),
          },
          italic: {
            type: 'boolean',
            description: localize(
              'schema.token.italic',
              "Sets or unsets the font style to italic. Note, the presence of 'fontStyle' overrides this setting.",
            ),
          },
          underline: {
            type: 'boolean',
            description: localize(
              'schema.token.underline',
              "Sets or unsets the font style to underline. Note, the presence of 'fontStyle' overrides this setting.",
            ),
          },
        },
        defaultSnippets: [{ body: { foreground: '${1:#FF0000}', fontStyle: '${2:bold}' } }],
      },
    },
  };

  parseTokenSelector(selectorString: string, language?: string): TokenSelector {
    const selector = parseClassifierString(selectorString, language);

    if (!selector.type) {
      return {
        match: () => -1,
        id: '$invalid',
      };
    }

    return {
      match: (type: string, modifiers: string[], language: string) => {
        let score = 0;
        if (selector.language !== undefined) {
          if (selector.language !== language) {
            return -1;
          }
          score += 10;
        }
        if (selector.type !== TOKEN_TYPE_WILDCARD) {
          const hierarchy = this.getTypeHierarchy(type);
          const level = hierarchy.indexOf(selector.type);
          if (level === -1) {
            return -1;
          }
          score += 100 - level;
        }
        // all selector modifiers must be present
        for (const selectorModifier of selector.modifiers) {
          if (modifiers.indexOf(selectorModifier) === -1) {
            return -1;
          }
        }
        return score + selector.modifiers.length * 100;
      },
      id: `${[selector.type, ...selector.modifiers.sort()].join('.')}${
        selector.language !== undefined ? ':' + selector.language : ''
      }`,
    };
  }

  registerTokenStyleDefault(selector: TokenSelector, defaults: TokenStyleDefaults): void {
    this.tokenStylingDefaultRules.push({ selector, defaults });
  }

  deregisterTokenStyleDefault(selector: TokenSelector): void {
    const selectorString = selector.id;
    this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter((r) => r.selector.id !== selectorString);
  }

  getTokenStylingDefaultRules() {
    return this.tokenStylingDefaultRules;
  }

  registerTokenModifier(id: string, description: string, deprecationMessage?: string): void {
    if (!id.match(typeAndModifierIdPattern)) {
      throw new Error('Invalid token modifier id.');
    }

    const num = this.currentModifierBit;
    this.currentModifierBit = this.currentModifierBit * 2;
    const tokenStyleContribution: TokenTypeOrModifierContribution = {
      num,
      id,
      description,
      deprecationMessage,
    };
    this.tokenModifierById[id] = tokenStyleContribution;

    this.tokenStylingSchema.properties[`*.${id}`] = getStylingSchemeEntry(description, deprecationMessage);
  }

  registerTokenType(id: string, description: string, superType?: string, deprecationMessage?: string): void {
    if (!id.match(typeAndModifierIdPattern)) {
      throw new Error('Invalid token type id.');
    }
    if (superType && !superType.match(typeAndModifierIdPattern)) {
      throw new Error('Invalid token super type id.');
    }

    const num = this.currentTypeNumber++;
    const tokenStyleContribution: TokenTypeOrModifierContribution = {
      num,
      id,
      superType,
      description,
      deprecationMessage,
    };
    this.tokenTypeById[id] = tokenStyleContribution;

    const stylingSchemeEntry = getStylingSchemeEntry(description, deprecationMessage);
    this.tokenStylingSchema.properties[id] = stylingSchemeEntry;
    this.typeHierarchy = Object.create(null);
  }

  private getTypeHierarchy(typeId: string): string[] {
    let hierarchy = this.typeHierarchy[typeId];
    if (!hierarchy) {
      this.typeHierarchy[typeId] = hierarchy = [typeId];
      let type = this.tokenTypeById[typeId];
      while (type && type.superType) {
        hierarchy.push(type.superType);
        type = this.tokenTypeById[type.superType];
      }
    }
    return hierarchy;
  }
}
