import { LitElement } from 'lit'
import { property } from 'lit/decorators.js'

// Match event type name strings that are registered on GlobalEventHandlersEventMap...
type EventTypeRequiresDetail<T> = T extends keyof GlobalEventHandlersEventMap
    ? // ...where the event detail is an object...
      GlobalEventHandlersEventMap[T] extends CustomEvent<Record<PropertyKey, unknown>>
        ? // ...that is non-empty...
          GlobalEventHandlersEventMap[T] extends CustomEvent<
              Record<PropertyKey, never>
          >
            ? never
            : // ...and has at least one non-optional property
              Partial<
                    GlobalEventHandlersEventMap[T]['detail']
                > extends GlobalEventHandlersEventMap[T]['detail']
              ? never
              : T
        : never
    : never

// The inverse of the above (match any type that doesn't match EventTypeRequiresDetail)
type EventTypeDoesNotRequireDetail<T> = T extends keyof GlobalEventHandlersEventMap
    ? GlobalEventHandlersEventMap[T] extends CustomEvent<Record<PropertyKey, unknown>>
        ? GlobalEventHandlersEventMap[T] extends CustomEvent<
              Record<PropertyKey, never>
          >
            ? T
            : Partial<
                    GlobalEventHandlersEventMap[T]['detail']
                > extends GlobalEventHandlersEventMap[T]['detail']
              ? T
              : never
        : T
    : T

// `keyof EventTypesWithRequiredDetail` lists all registered event types that require detail
type EventTypesWithRequiredDetail = {
    [EventType in keyof GlobalEventHandlersEventMap as EventTypeRequiresDetail<EventType>]: true
}

// `keyof EventTypesWithoutRequiredDetail` lists all registered event types that do NOT require detail
type EventTypesWithoutRequiredDetail = {
    [EventType in keyof GlobalEventHandlersEventMap as EventTypeDoesNotRequireDetail<EventType>]: true
}

// Helper to make a specific property of an object non-optional
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }

// Given an event name string, get a valid type for the options to initialize the event that is more restrictive than
// just CustomEventInit when appropriate (validate the type of the event detail, and require it to be provided if the
// event requires it)
type SlEventInit<T> = T extends keyof GlobalEventHandlersEventMap
    ? GlobalEventHandlersEventMap[T] extends CustomEvent<Record<PropertyKey, unknown>>
        ? GlobalEventHandlersEventMap[T] extends CustomEvent<
              Record<PropertyKey, never>
          >
            ? CustomEventInit<GlobalEventHandlersEventMap[T]['detail']>
            : Partial<
                    GlobalEventHandlersEventMap[T]['detail']
                > extends GlobalEventHandlersEventMap[T]['detail']
              ? CustomEventInit<GlobalEventHandlersEventMap[T]['detail']>
              : WithRequired<
                    CustomEventInit<GlobalEventHandlersEventMap[T]['detail']>,
                    'detail'
                >
        : CustomEventInit
    : CustomEventInit

// Given an event name string, get the type of the event
type GetCustomEventType<T> = T extends keyof GlobalEventHandlersEventMap
    ? GlobalEventHandlersEventMap[T] extends CustomEvent<unknown>
        ? GlobalEventHandlersEventMap[T]
        : CustomEvent<unknown>
    : CustomEvent<unknown>

// `keyof ValidEventTypeMap` is equivalent to `keyof GlobalEventHandlersEventMap` but gives a nicer error message
type ValidEventTypeMap =
    | EventTypesWithRequiredDetail
    | EventTypesWithoutRequiredDetail

export default class TerraElement extends LitElement {
    // Make localization attributes reactive
    @property() dir: string
    @property() lang: string

    /** Emits a custom event with more convenient defaults. */
    emit<T extends string & keyof EventTypesWithoutRequiredDetail>(
        name: EventTypeDoesNotRequireDetail<T>,
        options?: SlEventInit<T> | undefined
    ): GetCustomEventType<T>
    emit<T extends string & keyof EventTypesWithRequiredDetail>(
        name: EventTypeRequiresDetail<T>,
        options: SlEventInit<T>
    ): GetCustomEventType<T>
    emit<T extends string & keyof ValidEventTypeMap>(
        name: T,
        options?: SlEventInit<T> | undefined
    ): GetCustomEventType<T> {
        const event = new CustomEvent(name, {
            bubbles: true,
            cancelable: false,
            composed: true,
            detail: {},
            ...options,
        })

        this.dispatchEvent(event)

        return event as GetCustomEventType<T>
    }

    /* eslint-disable */
    // @ts-expect-error This is auto-injected at build time.
    static version = __COMPONENTS_VERSION__
    /* eslint-enable */

    static define(
        name: string,
        elementConstructor = this,
        options: ElementDefinitionOptions = {}
    ) {
        const currentlyRegisteredConstructor = customElements.get(name) as
            | CustomElementConstructor
            | typeof TerraElement

        if (!currentlyRegisteredConstructor) {
            customElements.define(
                name,
                class extends elementConstructor {} as unknown as CustomElementConstructor,
                options
            )
            return
        }

        let newVersion = ' (unknown version)'
        let existingVersion = newVersion

        if ('version' in elementConstructor && elementConstructor.version) {
            newVersion = ' v' + elementConstructor.version
        }

        if (
            'version' in currentlyRegisteredConstructor &&
            currentlyRegisteredConstructor.version
        ) {
            existingVersion = ' v' + currentlyRegisteredConstructor.version
        }

        // Need to make sure we're not working with null or empty strings before doing version comparisons.
        if (newVersion && existingVersion && newVersion === existingVersion) {
            // If versions match, we don't need to warn anyone. Carry on.
            return
        }

        console.warn(
            `Attempted to register <${name}>${newVersion}, but <${name}>${existingVersion} has already been registered.`
        )
    }

    static dependencies: Record<string, typeof TerraElement> = {}

    constructor() {
        super()
        Object.entries(
            (this.constructor as typeof TerraElement).dependencies
        ).forEach(([name, component]) => {
            ;(this.constructor as typeof TerraElement).define(name, component)
        })
    }
}

export interface TerraFormControl extends TerraElement {
    // Form attributes
    name: string
    value: unknown
    disabled?: boolean
    defaultValue?: unknown
    defaultChecked?: boolean
    form?: string

    // Constraint validation attributes
    pattern?: string
    min?: number | string | Date
    max?: number | string | Date
    step?: number | 'any'
    required?: boolean
    minlength?: number
    maxlength?: number

    // Form validation properties
    readonly validity: ValidityState
    readonly validationMessage: string

    // Form validation methods
    checkValidity: () => boolean
    getForm: () => HTMLFormElement | null
    reportValidity: () => boolean
    setCustomValidity: (message: string) => void
}
