import { Component } from '@wc-toolkit/cem-utilities';
import { PackageLinkPhaseParams } from '@custom-elements-manifest/analyzer';

/**
 * @license
 * Copyright (c) 2019 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
 * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
 * Code distributed by Google as part of the polymer project is also
 * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
 */

/**
 * The top-level interface of a custom elements manifest file.
 *
 * Because custom elements are JavaScript classes, describing a custom element
 * may require describing arbitrary JavaScript concepts like modules, classes,
 * functions, etc. So custom elements manifests are capable of documenting
 * the elements in a package, as well as those JavaScript concepts.
 *
 * The modules described in a package should be the public entrypoints that
 * other packages may import from. Multiple modules may export the same object
 * via re-exports, but in most cases a package should document the single
 * canonical export that should be used.
 */
interface Package {
  /**
   * The version of the schema used in this file.
   */
  schemaVersion: string;

  /**
   * The Markdown to use for the main readme of this package.
   *
   * This can be used to override the readme used by Github or npm if that
   * file contains information irrelevant to custom element catalogs and
   * documentation viewers.
   */
  readme?: string;

  /**
   * An array of the modules this package contains.
   */
  modules: Array<Module>;

  /**
   * Whether the package is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

// This type may expand in the future to include JSON, CSS, or HTML
// modules.
type Module = JavaScriptModule;

interface JavaScriptModule {
  kind: 'javascript-module';

  /**
   * Path to the javascript file needed to be imported.
   * (not the path for example to a typescript file.)
   */
  path: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description of the module.
   */
  description?: string;

  /**
   * The declarations of a module.
   *
   * For documentation purposes, all declarations that are reachable from
   * exports should be described here. Ie, functions and objects that may be
   * properties of exported objects, or passed as arguments to functions.
   */
  declarations?: Array<Declaration>;

  /**
   * The exports of a module. This includes JavaScript exports and
   * custom element definitions.
   */
  exports?: Array<Export>;

  /**
   * Whether the module is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

type Export = JavaScriptExport | CustomElementExport;

interface JavaScriptExport {
  kind: 'js';

  /**
   * The name of the exported symbol.
   *
   * JavaScript has a number of ways to export objects which determine the
   * correct name to use.
   *
   * - Default exports must use the name "default".
   * - Named exports use the name that is exported. If the export is renamed
   *   with the "as" clause, use the exported name.
   * - Aggregating exports (`* from`) should use the name `*`
   */
  name: string;

  /**
   * A reference to the exported declaration.
   *
   * In the case of aggregating exports, the reference's `module` field must be
   * defined and the `name` field must be `"*"`.
   */
  declaration: Reference;

  /**
   * Whether the export is deprecated. For example, the name of the export was changed.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

/**
 * A global custom element defintion, ie the result of a
 * `customElements.define()` call.
 *
 * This is represented as an export because a definition makes the element
 * available outside of the module it's defined it.
 */
interface CustomElementExport {
  kind: 'custom-element-definition';

  /**
   * The tag name of the custom element.
   */
  name: string;

  /**
   * A reference to the class or other declaration that implements the
   * custom element.
   */
  declaration: Reference;

  /**
   * Whether the custom-element export is deprecated.
   * For example, a future version will not register the custom element in this file.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

type Declaration =
  | ClassDeclaration
  | FunctionDeclaration
  | MixinDeclaration
  | VariableDeclaration
  | CustomElementDeclaration
  | CustomElementMixinDeclaration;

/**
 * A reference to an export of a module.
 *
 * All references are required to be publically accessible, so the canonical
 * representation of a reference is the export it's available from.
 *
 * `package` should generally refer to an npm package name. If `package` is
 * undefined then the reference is local to this package. If `module` is
 * undefined the reference is local to the containing module.
 *
 * References to global symbols like `Array`, `HTMLElement`, or `Event` should
 * use a `package` name of `"global:"`.
 */
interface Reference {
  name: string;
  package?: string;
  module?: string;
}

/**
 * A reference to the source of a declaration or member.
 */
interface SourceReference {
  /**
   * An absolute URL to the source (ie. a GitHub URL).
   */
  href: string;
}

/**
 * A description of a custom element class.
 *
 * Custom elements are JavaScript classes, so this extends from
 * `ClassDeclaration` and adds custom-element-specific features like
 * attributes, events, and slots.
 *
 * Note that `tagName` in this interface is optional. Tag names are not
 * neccessarily part of a custom element class, but belong to the definition
 * (often called the "registration") or the `customElements.define()` call.
 *
 * Because classes and tag names can only be registered once, there's a
 * one-to-one relationship between classes and tag names. For ease of use,
 * we allow the tag name here.
 *
 * Some packages define and register custom elements in separate modules. In
 * these cases one `Module` should contain the `CustomElement` without a
 * tagName, and another `Module` should contain the
 * `CustomElementExport`.
 */
// Note: this needs to be an interface to be included in the generated JSON
// Schema output.
interface CustomElementDeclaration
  extends ClassDeclaration,
    CustomElement {}

/**
 * The additional fields that a custom element adds to classes and mixins.
 */
interface CustomElement extends ClassLike {
  /**
   * An optional tag name that should be specified if this is a
   * self-registering element.
   *
   * Self-registering elements must also include a CustomElementExport
   * in the module's exports.
   */
  tagName?: string;

  /**
   * The attributes that this element is known to understand.
   */
  attributes?: Attribute[];

  /**
   * The events that this element fires.
   */
  events?: Event[];

  /**
   * The shadow dom content slots that this element accepts.
   */
  slots?: Slot[];

  cssParts?: CssPart[];

  cssProperties?: CssCustomProperty[];

  cssStates?: CssCustomState[];

  demos?: Demo[];

  /**
   * Distinguishes a regular JavaScript class from a
   * custom element class
   */
  customElement: true;
}

interface Attribute {
  name: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description.
   */
  description?: string;

  inheritedFrom?: Reference;

  /**
   * The type that the attribute will be serialized/deserialized as.
   */
  type?: Type;

  /**
   * The default value of the attribute, if any.
   *
   * As attributes are always strings, this is the actual value, not a human
   * readable description.
   */
  default?: string;

  /**
   * The name of the field this attribute is associated with, if any.
   */
  fieldName?: string;

  /**
   * Whether the attribute is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

interface Event {
  name: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description.
   */
  description?: string;

  /**
   * The type of the event object that's fired.
   */
  type: Type;

  inheritedFrom?: Reference;

  /**
   * Whether the event is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

interface Slot {
  /**
   * The slot name, or the empty string for an unnamed slot.
   */
  name: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description.
   */
  description?: string;

  /**
   * Whether the slot is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

/**
 * The description of a CSS Part
 */
interface CssPart {
  name: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description.
   */
  description?: string;

  /**
   * Whether the CSS shadow part is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

/**
 * The description of a CSS Custom State
 * https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet
 */
interface CssCustomState {
  /**
   * The name of the state. Note: Unlike CSS custom properties, custom states
   * do not have a leading `--`.
   */
  name: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description.
   */
  description?: string;

  /**
   * Whether the CSS custom state is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

interface CssCustomProperty {
  /**
   * The name of the property, including leading `--`.
   */
  name: string;

  /**
   * The expected syntax of the defined property. Defaults to "*".
   *
   * The syntax must be a valid CSS [syntax string](https://developer.mozilla.org/en-US/docs/Web/CSS/@property/syntax)
   * as defined in the CSS Properties and Values API.
   *
   * Examples:
   *
   * "<color>": accepts a color
   * "<length> | <percentage>": accepts lengths or percentages but not calc expressions with a combination of the two
   * "small | medium | large": accepts one of these values set as custom idents.
   * "*": any valid token
   */
  syntax?: string;

  default?: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description.
   */
  description?: string;

  /**
   * Whether the CSS custom property is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

interface Type {
  /**
   * The full string representation of the type, in whatever type syntax is
   * used, such as JSDoc, Closure, or TypeScript.
   */
  text: string;

  /**
   * An array of references to the types in the type string.
   *
   * These references have optional indices into the type string so that tools
   * can understand the references in the type string independently of the type
   * system and syntax. For example, a documentation viewer could display the
   * type `Array<FooElement | BarElement>` with cross-references to `FooElement`
   * and `BarElement` without understanding arrays, generics, or union types.
   */
  references?: TypeReference[];

  source?: SourceReference;
}

/**
 * A reference that is associated with a type string and optionally a range
 * within the string.
 *
 * Start and end must both be present or not present. If they're present, they
 * are indices into the associated type string. If they are missing, the entire
 * type string is the symbol referenced and the name should match the type
 * string.
 */
interface TypeReference extends Reference {
  start?: number;
  end?: number;
}

/**
 * The common interface of classes and mixins.
 */
interface ClassLike {
  name: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description of the class.
   */
  description?: string;

  /**
   * The superclass of this class.
   *
   * If this class is defined with mixin applications, the prototype chain
   * includes the mixin applications and the true superclass is computed
   * from them.
   */
  superclass?: Reference;

  /**
   * Any class mixins applied in the extends clause of this class.
   *
   * If mixins are applied in the class definition, then the true superclass
   * of this class is the result of applying mixins in order to the superclass.
   *
   * Mixins must be listed in order of their application to the superclass or
   * previous mixin application. This means that the innermost mixin is listed
   * first. This may read backwards from the common order in JavaScript, but
   * matches the order of language used to describe mixin application, like
   * "S with A, B".
   *
   * @example
   *
   * ```javascript
   * class T extends B(A(S)) {}
   * ```
   *
   * is described by:
   * ```json
   * {
   *   "kind": "class",
   *   "superclass": {
   *     "name": "S"
   *   },
   *   "mixins": [
   *     {
   *       "name": "A"
   *     },
   *     {
   *       "name": "B"
   *     },
   *   ]
   * }
   * ```
   */
  mixins?: Array<Reference>;
  members?: Array<ClassMember>;

  source?: SourceReference;

  /**
   * Whether the class or mixin is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;
}

interface ClassDeclaration extends ClassLike {
  kind: 'class';
}

type ClassMember = ClassField | ClassMethod;

/**
 * The common interface of variables, class fields, and function
 * parameters.
 */
interface PropertyLike {
  name: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description of the field.
   */
  description?: string;

  type?: Type;

  default?: string;

  /**
   * Whether the property is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;

  /**
   * Whether the property is read-only.
   */
  readonly?: boolean;
}

interface ClassField extends PropertyLike {
  kind: 'field';
  static?: boolean;
  privacy?: Privacy;
  inheritedFrom?: Reference;
  source?: SourceReference;
}

interface ClassMethod extends FunctionLike {
  kind: 'method';
  static?: boolean;
  privacy?: Privacy;
  inheritedFrom?: Reference;
  source?: SourceReference;
}

/**
 * A description of a class mixin.
 *
 * Mixins are functions which generate a new subclass of a given superclass.
 * This interfaces describes the class and custom element features that
 * are added by the mixin. As such, it extends the CustomElement interface and
 * ClassLike interface.
 *
 * Since mixins are functions, it also extends the FunctionLike interface. This
 * means a mixin is callable, and has parameters and a return type.
 *
 * The return type is often hard or impossible to accurately describe in type
 * systems like TypeScript. It requires generics and an `extends` operator
 * that TypeScript lacks. Therefore it's recommended that the return type is
 * left empty. The most common form of a mixin function takes a single
 * argument, so consumers of this interface should assume that the return type
 * is the single argument subclassed by this declaration.
 *
 * A mixin should not have a superclass. If a mixins composes other mixins,
 * they should be listed in the `mixins` field.
 *
 * See [this article]{@link https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/}
 * for more information on the classmixin pattern in JavaScript.
 *
 * @example
 *
 * This JavaScript mixin declaration:
 * ```javascript
 * const MyMixin = (base) => class extends base {
 *   foo() { ... }
 * }
 * ```
 *
 * Is described by this JSON:
 * ```json
 * {
 *   "kind": "mixin",
 *   "name": "MyMixin",
 *   "parameters": [
 *     {
 *       "name": "base",
 *     }
 *   ],
 *   "members": [
 *     {
 *       "kind": "method",
 *       "name": "foo",
 *     }
 *   ]
 * }
 * ```
 */
interface MixinDeclaration extends ClassLike, FunctionLike {
  kind: 'mixin';
}

/**
 * A class mixin that also adds custom element related properties.
 */
// Note: this needs to be an interface to be included in the generated JSON
// Schema output.
interface CustomElementMixinDeclaration
  extends MixinDeclaration,
    CustomElement {}

interface VariableDeclaration extends PropertyLike {
  kind: 'variable';
  source?: SourceReference;
}

interface FunctionDeclaration extends FunctionLike {
  kind: 'function';
  source?: SourceReference;
}

interface Parameter extends PropertyLike {
  /**
   * Whether the parameter is optional. Undefined implies non-optional.
   */
  optional?: boolean;
  /**
   * Whether the parameter is a rest parameter. Only the last parameter may be a rest parameter.
   * Undefined implies single parameter.
   */
  rest?: boolean;
}

interface FunctionLike {
  name: string;

  /**
   * A markdown summary suitable for display in a listing.
   */
  summary?: string;

  /**
   * A markdown description.
   */
  description?: string;

  /**
   * Whether the function is deprecated.
   * If the value is a string, it's the reason for the deprecation.
   */
  deprecated?: boolean | string;

  parameters?: Parameter[];

  return?: {
    type?: Type;

    /**
     * A markdown summary suitable for display in a listing.
     */
    summary?: string;

    /**
     * A markdown description.
     */
    description?: string;
  };
}

type Privacy = 'public' | 'private' | 'protected';

interface Demo {
  /**
   * A markdown description of the demo.
   */
  description?: string;

  /**
   * Relative URL of the demo if it's published with the package. Absolute URL
   * if it's hosted.
   */
  url: string;

  source?: SourceReference;
}

type CemValidatorOptions = {
    /** The path to the `package.json` file */
    packageJsonPath?: string;
    /** Custom Elements Manifest file name */
    cemFileName?: string;
    /** This will log errors rather throw an exception */
    logErrors?: boolean;
    /** Enables logging during the component loading process */
    debug?: boolean;
    /** Prevents plugin from executing */
    skip?: boolean;
    /** Rule configurations */
    rules?: Rules;
};
/** The severity level for each rule */
type Severity = "off" | "warning" | "error";
type Rules = {
    /** Checks if the package.json file is appropriately configured */
    packageJson?: {
        /** Is `type` property set to "module" */
        packageType?: Severity;
        /** Is `main` property set with a valid file path */
        main?: Severity;
        /** Is `module` property set with a valid file path */
        module?: Severity;
        /** Is `types` property set with a valid file path */
        types?: Severity;
        /** Does the package have a `exports` property configured */
        exports?: Severity;
        /** Is the `customElements` property properly configured */
        customElementsProperty?: Severity;
        /** Is the Custom Elements Manifest included in the published package */
        publishedCem?: Severity;
    };
    /** Checks if the `customElementsManifest` is valid */
    manifest?: {
        /** Is the manifest using the latest schema version */
        schemaVersion?: Severity;
        /** Does the component have a valid module path */
        modulePath?: Severity;
        /** Does the component have a valid definition path */
        definitionPath?: Severity;
        /** Does the element have a valid type definition path */
        typeDefinitionPath?: Severity;
        /** Does the component export all necessary types */
        exportTypes?: Severity;
        /** Does the component have a tag name defined */
        tagName?: Severity;
    };
};
type RuleFailure = {
    /** The rule that failed */
    rule: string;
    /** The severity of the rule */
    severity: Severity;
    /** The message of the rule */
    message: string;
};

declare let failures: RuleFailure[];
declare function validateCem(cem: unknown, options?: CemValidatorOptions): void;
declare function testPackageJson(packageJson: any): void;
declare function testManifest(manifest: Package): Promise<void>;
declare function testComponents(manifest: Package): void;
declare function testPackageType(moduleType: string, severity: Severity): void;
declare function testMainProperty(main: string, severity: Severity): void;
declare function testModuleProperty(module: string, severity: Severity): void;
declare function testTypesProperty(types: string, severity: Severity): void;
declare function testExportsProperty(exports: string, severity: Severity): void;
declare function testCustomElementsProperty(customElements: string, severity: Severity): void;
declare function testCemPublished(files: string[], cemFileName: string, severity: Severity): void;
declare function testSchemaVersion(schemaVersion: string, severity: Severity): Promise<void>;
declare function testComponentModulePath(componentName: string, modulePath: string, severity: Severity): void;
declare function testComponentDefinitionPath(componentName: string, definitionPath: string, severity: Severity): void;
declare function testComponentTypeDefinitionPath(componentName: string, definitionPath: string, severity: Severity): void;
declare function testComponentExportTypes(component: Component, exports: string[], severity: Severity): void;
declare function testComponentTagName(componentName: string, tagName: string, severity: Severity): void;

declare function cemValidatorPlugin(options?: CemValidatorOptions): {
    name: string;
    packageLinkPhase({ customElementsManifest }: PackageLinkPhaseParams): void;
};

export { type CemValidatorOptions, type RuleFailure, type Rules, type Severity, cemValidatorPlugin, failures, testCemPublished, testComponentDefinitionPath, testComponentExportTypes, testComponentModulePath, testComponentTagName, testComponentTypeDefinitionPath, testComponents, testCustomElementsProperty, testExportsProperty, testMainProperty, testManifest, testModuleProperty, testPackageJson, testPackageType, testSchemaVersion, testTypesProperty, validateCem };
