/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { LitElement, html, TemplateResult, CSSResult, PropertyValues } from 'lit';
import { customElement, property, queryAsync, eventOptions } from 'lit/decorators';
import { ifDefined } from 'lit/directives/if-defined.js';
import { Ripple } from '@material/mwc-ripple';
import { RippleHandlers } from '@material/mwc-ripple/ripple-handlers';
import { styleMap, StyleInfo } from 'lit-html/directives/style-map';
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
import { classMap, ClassInfo } from 'lit-html/directives/class-map';
import { HassEntity } from 'home-assistant-js-websocket';
import { LovelaceCard } from './types/lovelace';
import {
  ButtonCardConfig,
  ExternalButtonCardConfig,
  StateConfig,
  ExemptionUserConfig,
  ExemptionUsernameConfig,
  CustomFieldCard,
  ButtonCardEmbeddedCards,
  ButtonCardEmbeddedCardsConfig,
  ColorType,
} from './types/types';
import { actionHandler } from './action-handler';
import {
  computeDomain,
  computeEntity,
  getFontColorBasedOnBackgroundColor,
  buildNameStateConcat,
  applyBrightnessToColor,
  myHasConfigOrEntityChanged,
  getLightColorBasedOnTemperature,
  mergeDeep,
  mergeStatesById,
  getLovelace,
  getLovelaceCast,
  secondsToDuration,
  durationToSeconds,
  computeStateDomain,
  stateActive,
  computeCssVariable,
} from './helpers';
import { createThing } from './common/create-thing';
import { styles } from './styles';
import { computeStateDisplay } from './common/compute_state_display';
import copy from 'fast-copy';
import * as pjson from '../package.json';
import { deepEqual } from './deep-equal';
import { stateColorCss } from './common/state_color';
import {
  AUTO_COLORS,
  DEFAULT_COLOR,
  DOMAINS_TOGGLE,
  OVERRIDE_CARD_BACKGROUND_COLOR_COLORS,
  OVERRIDE_CARD_BACKGROUND_COLOR_COLOR_TYPE,
} from './common/const';
import { handleAction } from './handle-action';
import { fireEvent } from './common/fire-event';
import { HomeAssistant } from './types/homeassistant';
import { timerTimeRemaining } from './common/timer';
import {
  formatDateTime,
  formatDateTimeNumeric,
  formatDateTimeWithSeconds,
  formatShortDateTime,
  formatShortDateTimeWithYear,
} from './common/format_date_time';
import { formatTime, formatTime24h, formatTimeWeekday, formatTimeWithSeconds } from './common/format_time';
import {
  formatDate,
  formatDateMonth,
  formatDateMonthYear,
  formatDateNumeric,
  formatDateShort,
  formatDateWeekday,
  formatDateWeekdayDay,
  formatDateWeekdayShort,
  formatDateYear,
} from './common/format_date';

let helpers = (window as any).cardHelpers;
const helperPromise = new Promise<void>(async (resolve) => {
  if (helpers) resolve();
  if ((window as any).loadCardHelpers) {
    helpers = await (window as any).loadCardHelpers();
    (window as any).cardHelpers = helpers;
    resolve();
  }
});

/* eslint no-console: 0 */
console.info(
  `%c  BUTTON-CARD  \n%c Version ${pjson.version} `,
  'color: orange; font-weight: bold; background: black',
  'color: white; font-weight: bold; background: dimgray',
);

(window as any).customCards = (window as any).customCards || [];
(window as any).customCards.push({
  type: 'button-card',
  name: 'Button-Card',
  preview: false,
  description: 'A massively customizable custom button card',
});

@customElement('button-card')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class ButtonCard extends LitElement {
  @property() private _hass?: HomeAssistant;

  @property() private _config?: ButtonCardConfig;

  @property() private _timeRemaining?: number;

  @queryAsync('mwc-ripple') private _ripple!: Promise<Ripple | null>;

  private _hasTemplate?: boolean;

  private _stateObj: HassEntity | undefined;

  private _evaledVariables: any | undefined;

  private _interval?: number;

  private _cards: ButtonCardEmbeddedCards = {};

  private _cardsConfig: ButtonCardEmbeddedCardsConfig = {};

  private _entities: string[] = [];

  private _initialSetupComplete = false;

  private get _doIHaveEverything(): boolean {
    return !!this._hass && !!this._config && this.isConnected;
  }

  public set hass(hass: HomeAssistant) {
    this._hass = hass;
    Object.keys(this._cards).forEach((element) => {
      const el = this._cards[element];
      el.hass = this._hass;
    });
    if (!this._initialSetupComplete) {
      this._finishSetup();
    }
  }

  public disconnectedCallback(): void {
    super.disconnectedCallback();
    this._clearInterval();
  }

  public connectedCallback(): void {
    super.connectedCallback();
    if (!this._initialSetupComplete) {
      this._finishSetup();
    } else {
      this._startTimerCountdown();
    }
  }

  private _evaluateVariablesSkipError(stateObj?: HassEntity | undefined) {
    this._evaledVariables = {};
    if (this._config?.variables) {
      const variablesNameOrdered = Object.keys(this._config.variables).sort();
      variablesNameOrdered.forEach((variable) => {
        try {
          this._evaledVariables[variable] = this._objectEvalTemplate(stateObj, this._config!.variables![variable]);
        } catch (e) {}
      });
    }
  }

  private _finishSetup(): void {
    if (!this._initialSetupComplete && this._doIHaveEverything) {
      this._evaluateVariablesSkipError();

      if (this._config!.entity) {
        const entityEvaled = this._getTemplateOrValue(undefined, this._config!.entity);
        this._config!.entity = entityEvaled;
        this._stateObj = this._hass!.states[entityEvaled];
      }

      this._evaluateVariablesSkipError(this._stateObj);

      if (this._config!.entity && DOMAINS_TOGGLE.has(computeDomain(this._config!.entity))) {
        this._config = {
          tap_action: { action: 'toggle' },
          ...this._config!,
        };
      } else if (this._config!.entity) {
        this._config = {
          tap_action: { action: 'more-info' },
          ...this._config!,
        };
      } else {
        this._config = {
          tap_action: { action: 'none' },
          ...this._config!,
        };
      }

      const jsonConfig = JSON.stringify(this._config);
      this._entities = [];
      if (Array.isArray(this._config!.triggers_update)) {
        this._config!.triggers_update.forEach((entry) => {
          try {
            const evaluatedEntry = this._getTemplateOrValue(this._stateObj, entry);
            if (evaluatedEntry !== undefined && evaluatedEntry !== null && !this._entities.includes(evaluatedEntry)) {
              this._entities.push(evaluatedEntry);
            }
          } catch (e) {}
        });
      } else if (typeof this._config!.triggers_update === 'string') {
        const result = this._getTemplateOrValue(this._stateObj, this._config!.triggers_update);
        if (result && result !== 'all') {
          this._entities.push(result);
        } else {
          this._config.triggers_update = result;
        }
      }
      if (this._config!.triggers_update !== 'all') {
        const entitiesRxp = new RegExp(/states\[\s*('|\\")([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)\1\s*\]/, 'gm');
        const entitiesRxp2 = new RegExp(/states\[\s*('|\\")([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)\1\s*\]/, 'm');
        const matched = jsonConfig.match(entitiesRxp);
        matched?.forEach((match) => {
          const res = match.match(entitiesRxp2);
          if (res && !this._entities.includes(res[2])) this._entities.push(res[2]);
        });
      }
      if (this._config!.entity && !this._entities.includes(this._config!.entity))
        this._entities.push(this._config!.entity);
      this._expandTriggerGroups();

      const rxp = new RegExp('\\[\\[\\[.*\\]\\]\\]', 'm');
      this._hasTemplate = this._config!.triggers_update === 'all' && jsonConfig.match(rxp) ? true : false;

      this._startTimerCountdown();
      this._initialSetupComplete = true;
    }
  }

  private _startTimerCountdown(): void {
    if (this._config && this._config.entity && computeDomain(this._config.entity) === 'timer') {
      const stateObj = this._hass!.states[this._config.entity];
      this._startInterval(stateObj);
    }
  }

  private _createCard(config: any): any {
    if (helpers) return helpers.createCardElement(config);
    else {
      const element = createThing(config);
      helperPromise.then(() => {
        fireEvent(element, 'll-rebuild', {});
      });
      return element;
    }
  }

  static get styles(): CSSResult {
    return styles;
  }

  protected render(): TemplateResult | void {
    if (!this._config || !this._hass) return html``;
    this._stateObj = this._config!.entity ? this._hass!.states[this._config!.entity] : undefined;
    try {
      this._evaledVariables = {};
      if (this._config?.variables) {
        const variablesNameOrdered = Object.keys(this._config.variables).sort();
        variablesNameOrdered.forEach((variable) => {
          this._evaledVariables[variable] = this._objectEvalTemplate(
            this._stateObj,
            this._config!.variables![variable],
          );
        });
      }
      return this._cardHtml();
    } catch (e: any) {
      if (e.stack) console.error(e.stack);
      else console.error(e);
      const errorCard = document.createElement('hui-error-card') as LovelaceCard;
      errorCard.setConfig({
        type: 'error',
        error: e.toString(),
        origConfig: this._config,
      });
      return html` ${errorCard} `;
    }
  }

  protected shouldUpdate(changedProps: PropertyValues): boolean {
    const forceUpdate = this._hasTemplate || changedProps.has('_timeRemaining') ? true : false;
    if (forceUpdate || myHasConfigOrEntityChanged(this, changedProps)) {
      this._expandTriggerGroups();
      return true;
    }
    return false;
  }

  protected updated(changedProps: PropertyValues): void {
    super.updated(changedProps);

    if (
      this._config &&
      this._config.entity &&
      computeDomain(this._config.entity) === 'timer' &&
      changedProps.has('_hass')
    ) {
      const stateObj = this._hass!.states[this._config.entity];
      const oldHass = changedProps.get('_hass') as HomeAssistant;
      const oldStateObj = oldHass ? oldHass.states[this._config.entity] : undefined;

      if (oldStateObj !== stateObj) {
        this._startInterval(stateObj);
      } else if (!stateObj) {
        this._clearInterval();
      }
    }
  }

  private _clearInterval(): void {
    if (this._interval) {
      window.clearInterval(this._interval);
      this._interval = undefined;
    }
  }

  private _startInterval(stateObj: HassEntity): void {
    this._clearInterval();
    this._calculateRemaining(stateObj);

    if (stateObj.state === 'active') {
      this._interval = window.setInterval(() => this._calculateRemaining(stateObj), 1000);
    }
  }

  private _calculateRemaining(stateObj: HassEntity): void {
    if (stateObj.attributes.remaining) {
      this._timeRemaining = timerTimeRemaining(stateObj);
    }
  }

  private _computeTimeDisplay(stateObj: HassEntity): string | undefined | null {
    if (!stateObj) {
      return undefined;
    }

    return secondsToDuration(this._timeRemaining || durationToSeconds(stateObj.attributes.duration));
  }

  private _getMatchingConfigState(state: HassEntity | undefined): StateConfig | undefined {
    if (!this._config!.state) {
      return undefined;
    }
    const hasTemplate = this._config!.state.find((elt) => elt.operator === 'template');
    if (!state && !hasTemplate) {
      return undefined;
    }
    let def: StateConfig | undefined;
    const retval = this._config!.state.find((elt) => {
      if (elt.operator) {
        switch (elt.operator) {
          case '==':
            /* eslint eqeqeq: 0 */
            return state && state.state == this._getTemplateOrValue(state, elt.value);
          case '<=':
            return state && state.state <= this._getTemplateOrValue(state, elt.value);
          case '<':
            return state && state.state < this._getTemplateOrValue(state, elt.value);
          case '>=':
            return state && state.state >= this._getTemplateOrValue(state, elt.value);
          case '>':
            return state && state.state > this._getTemplateOrValue(state, elt.value);
          case '!=':
            return state && state.state != this._getTemplateOrValue(state, elt.value);
          case 'regex': {
            /* eslint no-unneeded-ternary: 0 */
            const matches = state && state.state.match(this._getTemplateOrValue(state, elt.value)) ? true : false;
            return matches;
          }
          case 'template': {
            return this._getTemplateOrValue(state, elt.value);
          }
          case 'default':
            def = elt;
            return false;
          default:
            return false;
        }
      } else {
        return state && this._getTemplateOrValue(state, elt.value) == state.state;
      }
    });
    if (!retval && def) {
      return def;
    }
    return retval;
  }

  private _localize(
    stateObj: HassEntity,
    state?: string,
    numeric_precision?: number | 'card',
    show_units = true,
    units?: string,
  ): string {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    return computeStateDisplay(
      this._hass!.localize,
      stateObj,
      this._hass!.locale,
      this._hass!.config,
      this._hass!.entities,
      {
        numeric_precision: numeric_precision === 'card' ? this._config?.numeric_precision : numeric_precision,
        show_units,
        units,
      },
      state,
    );
  }

  private _relativeTime(date: string | undefined, capitalize: boolean = false) {
    if (date) {
      return html`
        <ha-relative-time
          id="relative-time"
          class="ellipsis"
          .hass="${this._hass}"
          .datetime="${date}"
          .capitalize="${capitalize}"
        ></ha-relative-time>
      `;
    }
    return '';
  }

  private _getTemplateHelpers() {
    return {
      localize: this._localize.bind(this),
      // Datetime functions
      formatDateTime: (datetime) => {
        return formatDateTime(new Date(datetime), this._hass!.locale, this._hass!.config);
      },
      formatShortDateTimeWithYear: (datetime) => {
        return formatShortDateTimeWithYear(new Date(datetime), this._hass!.locale, this._hass!.config);
      },
      formatShortDateTime: (datetime) => {
        return formatShortDateTime(new Date(datetime), this._hass!.locale, this._hass!.config);
      },
      formatDateTimeWithSeconds: (datetime) => {
        return formatDateTimeWithSeconds(new Date(datetime), this._hass!.locale, this._hass!.config);
      },
      formatDateTimeNumeric: (datetime) => {
        return formatDateTimeNumeric(new Date(datetime), this._hass!.locale, this._hass!.config);
      },
      // Time functions
      relativeTime: this._relativeTime.bind(this),
      formatTime: (time) => {
        return formatTime(new Date(time), this._hass!.locale, this._hass!.config);
      },
      formatTimeWithSeconds: (time) => {
        return formatTimeWithSeconds(new Date(time), this._hass!.locale, this._hass!.config);
      },
      formatTimeWeekday: (time) => {
        return formatTimeWeekday(new Date(time), this._hass!.locale, this._hass!.config);
      },
      formatTime24h: (time) => {
        return formatTime24h(new Date(time), this._hass!.locale, this._hass!.config);
      },
      // Date functions
      formatDateWeekdayDay: (date) => {
        return formatDateWeekdayDay(new Date(date), this._hass!.locale, this._hass!.config);
      },
      formatDate: (date) => {
        return formatDate(new Date(date), this._hass!.locale, this._hass!.config);
      },
      formatDateNumeric: (date) => {
        return formatDateNumeric(new Date(date), this._hass!.locale, this._hass!.config);
      },
      formatDateShort: (date) => {
        return formatDateShort(new Date(date), this._hass!.locale, this._hass!.config);
      },
      formatDateMonthYear: (date) => {
        return formatDateMonthYear(new Date(date), this._hass!.locale, this._hass!.config);
      },
      formatDateMonth: (date) => {
        return formatDateMonth(new Date(date), this._hass!.locale, this._hass!.config);
      },
      formatDateYear: (date) => {
        return formatDateYear(new Date(date), this._hass!.locale, this._hass!.config);
      },
      formatDateWeekday: (date) => {
        return formatDateWeekday(new Date(date), this._hass!.locale, this._hass!.config);
      },
      formatDateWeekdayShort: (date) => {
        return formatDateWeekdayShort(new Date(date), this._hass!.locale, this._hass!.config);
      },
    };
  }

  private _evalTemplate(state: HassEntity | undefined, func: any): any {
    /* eslint no-new-func: 0 */
    try {
      return new Function(
        'states',
        'entity',
        'user',
        'hass',
        'variables',
        'html',
        `helpers`,
        `'use strict'; ${func}`,
      ).call(
        this,
        this._hass!.states,
        state,
        this._hass!.user,
        this._hass,
        this._evaledVariables,
        html,
        this._getTemplateHelpers(),
      );
    } catch (e: any) {
      const funcTrimmed = func.length <= 100 ? func.trim() : `${func.trim().substring(0, 98)}...`;
      e.message = `${e.name}: ${e.message} in '${funcTrimmed}'`;
      e.name = 'ButtonCardJSTemplateError';
      throw e;
    }
  }

  private _objectEvalTemplate(state: HassEntity | undefined, obj: any | undefined): any {
    const objClone = copy(obj);
    return this._getTemplateOrValue(state, objClone);
  }

  private _getTemplateOrValue(state: HassEntity | undefined, value: any | undefined): any | undefined {
    if (['number', 'boolean'].includes(typeof value)) return value;
    if (!value) return value;
    if (typeof value === 'object') {
      Object.keys(value).forEach((key) => {
        value[key] = this._getTemplateOrValue(state, value[key]);
      });
      return value;
    }
    const trimmed = value.trim();
    if (trimmed.substring(0, 3) === '[[[' && trimmed.slice(-3) === ']]]') {
      return this._evalTemplate(state, trimmed.slice(3, -3));
    } else {
      return value;
    }
  }

  private _getColorForLightEntity(
    state: HassEntity | undefined,
    useTemperature: boolean,
    cardColorType?: ColorType,
  ): string {
    let color: string = DEFAULT_COLOR;
    if (OVERRIDE_CARD_BACKGROUND_COLOR_COLOR_TYPE.includes(color)) {
      color = computeCssVariable(OVERRIDE_CARD_BACKGROUND_COLOR_COLORS)!;
    }
    if (state) {
      // we have en entity
      if (stateActive(state)) {
        // entity is on
        if (state.attributes.rgb_color) {
          // entity has RGB attributes
          color = `rgb(${state.attributes.rgb_color.join(',')})`;
        } else if (
          useTemperature &&
          state.attributes.color_temp &&
          state.attributes.min_mireds &&
          state.attributes.max_mireds
        ) {
          // entity has color temperature and we want it
          color = getLightColorBasedOnTemperature(
            state.attributes.color_temp,
            state.attributes.min_mireds,
            state.attributes.max_mireds,
          );
        } else {
          // all the other lights
          color = stateColorCss(state, state.state, cardColorType) || DEFAULT_COLOR;
        }
        if (state.attributes.brightness) {
          color = applyBrightnessToColor(this, color, (state.attributes.brightness + 245) / 5);
        }
      } else {
        color = stateColorCss(state, state.state, cardColorType) || DEFAULT_COLOR;
      }
    }
    return color;
  }

  private _buildCssColorAttribute(state: HassEntity | undefined, configState: StateConfig | undefined): string {
    let colorValue = '';
    let color: undefined | string;
    if (configState?.color) {
      colorValue = configState.color;
    } else if (this._config!.color) {
      colorValue = this._config!.color;
    }
    if (AUTO_COLORS.includes(colorValue)) {
      if (!state || (state && !(computeDomain(state.entity_id) !== 'light'))) {
        colorValue = '';
      }
    }
    if (AUTO_COLORS.includes(colorValue)) {
      // I'm a light
      color = this._getColorForLightEntity(state, colorValue !== 'auto-no-temperature', this._config?.color_type);
    } else if (colorValue) {
      // Color is forced but not auto
      color = colorValue;
    } else if (state) {
      // based on state
      color = stateColorCss(state, state.state, this._config?.color_type) || DEFAULT_COLOR;
    } else {
      color = DEFAULT_COLOR;
    }
    return color;
  }

  private _buildIcon(state: HassEntity | undefined, configState: StateConfig | undefined): string | undefined {
    if (!this._config!.show_icon) {
      return undefined;
    }
    let icon: undefined | string;
    if (configState?.icon) {
      icon = configState.icon;
    } else if (this._config!.icon) {
      icon = this._config!.icon;
    } else {
      return undefined;
    }
    return this._getTemplateOrValue(state, icon);
  }

  private _buildEntityPicture(state: HassEntity | undefined, configState: StateConfig | undefined): string | undefined {
    if (!this._config!.show_entity_picture || (!state && !configState && !this._config!.entity_picture)) {
      return undefined;
    }
    let entityPicture: string | undefined;

    if (configState?.entity_picture) {
      entityPicture = configState.entity_picture;
    } else if (this._config!.entity_picture) {
      entityPicture = this._config!.entity_picture;
    } else if (state) {
      entityPicture = state.attributes && state.attributes.entity_picture ? state.attributes.entity_picture : undefined;
    }
    return this._getTemplateOrValue(state, entityPicture);
  }

  private _buildStyleGeneric(
    state: HassEntity | undefined,
    configState: StateConfig | undefined,
    styleType: string,
  ): StyleInfo {
    let style: any = {};
    if (this._config!.styles?.[styleType]) {
      style = Object.assign(style, ...this._config!.styles[styleType]);
    }
    if (configState?.styles?.[styleType]) {
      let configStateStyle: StyleInfo = {};
      configStateStyle = Object.assign(configStateStyle, ...configState.styles[styleType]);
      style = {
        ...style,
        ...configStateStyle,
      };
    }
    Object.keys(style).forEach((key) => {
      style[key] = this._getTemplateOrValue(state, style[key]);
    });
    return style;
  }

  private _buildCustomStyleGeneric(
    state: HassEntity | undefined,
    configState: StateConfig | undefined,
    styleType: string,
  ): StyleInfo {
    let style: any = {};
    if (this._config!.styles?.custom_fields?.[styleType]) {
      style = Object.assign(style, ...this._config!.styles.custom_fields[styleType]);
    }
    if (configState?.styles?.custom_fields?.[styleType]) {
      let configStateStyle: StyleInfo = {};
      configStateStyle = Object.assign(configStateStyle, ...configState.styles.custom_fields[styleType]);
      style = {
        ...style,
        ...configStateStyle,
      };
    }
    Object.keys(style).forEach((key) => {
      style[key] = this._getTemplateOrValue(state, style[key]);
    });
    return style;
  }

  private _buildName(state: HassEntity | undefined, configState: StateConfig | undefined): string | undefined {
    if (this._config!.show_name === false) {
      return undefined;
    }
    let name: string | undefined;

    if (configState?.name) {
      name = configState.name;
    } else if (this._config!.name) {
      name = this._config!.name;
    } else if (state) {
      name =
        state.attributes && state.attributes.friendly_name
          ? state.attributes.friendly_name
          : computeEntity(state.entity_id);
    }
    return this._getTemplateOrValue(state, name);
  }

  private _buildStateString(stateObj: HassEntity | undefined): string | undefined | null {
    let stateString: string | undefined | null;
    if (this._config!.show_state && stateObj && stateObj.state) {
      if (computeDomain(stateObj.entity_id) === 'timer') {
        if (stateObj.state === 'idle' || this._timeRemaining === 0) {
          stateString = computeStateDisplay(
            this._hass!.localize,
            stateObj,
            this._hass!.locale,
            this._hass!.config,
            this._hass!.entities,
            this._config,
          );
        } else {
          stateString = this._computeTimeDisplay(stateObj);
          if (stateObj.state === 'paused') {
            stateString += ` (${computeStateDisplay(
              this._hass!.localize,
              stateObj,
              this._hass!.locale,
              this._hass!.config,
              this._hass!.entities,
              this._config,
            )})`;
          }
        }
      } else {
        stateString = computeStateDisplay(
          this._hass!.localize,
          stateObj,
          this._hass!.locale,
          this._hass!.config,
          this._hass!.entities,
          this._config,
        );
      }
    }
    return stateString;
  }

  private _buildLastChanged(state: HassEntity | undefined, style: StyleInfo): TemplateResult | undefined {
    return this._config!.show_last_changed && state
      ? html`
          <ha-relative-time
            id="label"
            class="ellipsis"
            .hass="${this._hass}"
            .datetime="${state.last_changed}"
            style=${styleMap(style)}
          ></ha-relative-time>
        `
      : undefined;
  }

  private _buildLabel(state: HassEntity | undefined, configState: StateConfig | undefined): string | undefined {
    if (!this._config!.show_label) {
      return undefined;
    }
    let label: string | undefined;

    if (configState?.label) {
      label = configState.label;
    } else {
      label = this._config!.label;
    }

    return this._getTemplateOrValue(state, label);
  }

  private _buildCustomFields(state: HassEntity | undefined, configState: StateConfig | undefined): TemplateResult {
    let result = html``;
    const fields: any = {};
    const cards: any = {};
    if (this._config!.custom_fields) {
      Object.keys(this._config!.custom_fields).forEach((key) => {
        const value = this._config!.custom_fields![key];
        if (!(value as CustomFieldCard).card) {
          fields[key] = this._getTemplateOrValue(state, value);
        } else {
          if ((value as CustomFieldCard).do_not_eval) {
            cards[key] = copy((value as CustomFieldCard).card);
          } else {
            cards[key] = this._objectEvalTemplate(state, (value as CustomFieldCard).card);
          }
        }
      });
    }
    if (configState?.custom_fields) {
      Object.keys(configState.custom_fields).forEach((key) => {
        const value = configState!.custom_fields![key];
        if (!(value as CustomFieldCard)!.card) {
          fields[key] = this._getTemplateOrValue(state, value);
        } else {
          if ((value as CustomFieldCard).do_not_eval) {
            cards[key] = copy((value as CustomFieldCard).card);
          } else {
            cards[key] = this._objectEvalTemplate(state, (value as CustomFieldCard).card);
          }
        }
      });
    }
    Object.keys(fields).forEach((key) => {
      if (fields[key] != undefined) {
        const customStyle: StyleInfo = {
          ...this._buildCustomStyleGeneric(state, configState, key),
          'grid-area': key,
        };
        result = html`
          ${result}
          <div id=${key} class="ellipsis" style=${styleMap(customStyle)}>${this._unsafeHTMLorNot(fields[key])}</div>
        `;
      }
    });
    Object.keys(cards).forEach((key) => {
      if (cards[key] != undefined) {
        const customStyle: StyleInfo = {
          ...this._buildCustomStyleGeneric(state, configState, key),
          'grid-area': key,
        };
        let thing;
        if (!deepEqual(this._cardsConfig[key], cards[key])) {
          thing = this._createCard(cards[key]);
          this._cards[key] = thing;
          this._cardsConfig[key] = copy(cards[key]);
        } else {
          thing = this._cards[key];
        }
        thing.hass = this._hass;
        result = html`
          ${result}
          <div
            id=${key}
            @action=${this._stopPropagation}
            @click=${this._stopPropagation}
            @touchstart=${this._stopPropagation}
            @mousedown=${this._stopPropagation}
            @mouseup=${this._stopPropagation}
            @touchend=${this._stopPropagation}
            @touchcancel=${this._stopPropagation}
            style=${styleMap(customStyle)}
          >
            ${thing}
          </div>
        `;
      }
    });
    return result;
  }

  private _isClickable(state: HassEntity | undefined, configState: StateConfig | undefined): boolean {
    let clickable = true;
    const tap_action = this._getTemplateOrValue(state, this._config!.tap_action!.action);
    const hold_action = this._getTemplateOrValue(state, this._config!.hold_action!.action);
    const double_tap_action = this._getTemplateOrValue(state, this._config!.double_tap_action!.action);
    let hasChildCards = false;
    if (this._config!.custom_fields) {
      hasChildCards = Object.keys(this._config!.custom_fields).some((key) => {
        const value = this._config!.custom_fields![key];
        if ((value as CustomFieldCard)!.card) {
          return true;
        }
        return false;
      });
    }
    if (!hasChildCards && configState) {
      if (configState.custom_fields) {
        return (hasChildCards = Object.keys(configState.custom_fields).some((key) => {
          const value = configState.custom_fields![key];
          if ((value as CustomFieldCard)!.card) {
            return true;
          }
          return false;
        }));
      }
    }
    if (tap_action != 'none' || hold_action != 'none' || double_tap_action != 'none' || hasChildCards) {
      clickable = true;
    } else {
      clickable = false;
    }
    return clickable;
  }

  private _rotate(configState: StateConfig | undefined): boolean {
    return configState?.spin ? true : false;
  }

  private _blankCardColoredHtml(cardStyle: StyleInfo): TemplateResult {
    const blankCardStyle = {
      background: 'none',
      'box-shadow': 'none',
      'border-style': 'none',
      ...cardStyle,
    };
    return html`
      <ha-card class="disabled" style=${styleMap(blankCardStyle)}>
        <div></div>
      </ha-card>
    `;
  }

  private _cardHtml(): TemplateResult {
    const configState = this._getMatchingConfigState(this._stateObj);
    let color: string = 'var(--state-inactive-color)';
    if (!!configState?.color && !AUTO_COLORS.includes(configState.color)) {
      color = configState.color;
    } else if (!!this._config?.color && !AUTO_COLORS.includes(this._config.color)) {
      if (this._stateObj) {
        if (stateActive(this._stateObj)) {
          color = this._config?.color || color;
        }
      } else {
        color = this._config.color;
      }
    } else {
      color = this._buildCssColorAttribute(this._stateObj, configState);
    }
    let buttonColor = color;
    let cardStyle: any = {};
    let lockStyle: any = {};
    const aspectRatio: any = {};
    const lockStyleFromConfig = this._buildStyleGeneric(this._stateObj, configState, 'lock');
    const configCardStyle = this._buildStyleGeneric(this._stateObj, configState, 'card');
    const tooltipStyleFromConfig = this._buildStyleGeneric(this._stateObj, configState, 'tooltip');
    const classList: ClassInfo = {
      'button-card-main': true,
      disabled: !this._isClickable(this._stateObj, configState),
    };
    if (this._config?.tooltip) {
      this.classList.add('tooltip');
    }
    if (configCardStyle.width) {
      this.style.setProperty('flex', '0 0 auto');
      this.style.setProperty('max-width', 'fit-content');
    }
    switch (this._config!.color_type) {
      case 'blank-card':
        return this._blankCardColoredHtml(configCardStyle);
      case 'card':
      case 'label-card': {
        const fontColor = getFontColorBasedOnBackgroundColor(this, color);
        cardStyle.color = fontColor;
        lockStyle.color = fontColor;
        cardStyle['background-color'] = color;
        cardStyle = { ...cardStyle, ...configCardStyle };
        buttonColor = 'inherit';
        break;
      }
      default:
        cardStyle = configCardStyle;
        break;
    }
    if (this._config!.aspect_ratio) {
      aspectRatio['--aspect-ratio'] = this._config!.aspect_ratio;
      cardStyle.position = 'absolute';
    } else {
      aspectRatio.display = 'inline';
    }
    this.style.setProperty('--button-card-light-color', this._getColorForLightEntity(this._stateObj, true));
    this.style.setProperty(
      '--button-card-light-color-no-temperature',
      this._getColorForLightEntity(this._stateObj, false),
    );
    lockStyle = { ...lockStyle, ...lockStyleFromConfig };

    const extraStyles = this._config!.extra_styles
      ? html`
          <style>
            ${this._getTemplateOrValue(this._stateObj, this._config!.extra_styles)}
          </style>
        `
      : html``;
    return html`
      ${extraStyles}
      <div id="aspect-ratio" style=${styleMap(aspectRatio)}>
        <ha-card
          id="card"
          class=${classMap(classList)}
          style=${styleMap(cardStyle)}
          @action=${this._handleAction}
          @focus="${this.handleRippleFocus}"
          @blur="${this.handleRippleBlur}"
          @mousedown="${this.handleRippleActivate}"
          @mouseup="${this.handleRippleDeactivate}"
          @touchstart="${this.handleRippleActivate}"
          @touchend="${this.handleRippleDeactivate}"
          @touchcancel="${this.handleRippleDeactivate}"
          .actionHandler=${actionHandler({
            hasDoubleClick: this._config!.double_tap_action!.action !== 'none',
            hasHold: this._config!.hold_action!.action !== 'none',
            repeat: this._config!.hold_action!.repeat,
            repeatLimit: this._config!.hold_action!.repeat_limit,
          })}
          .config="${this._config}"
        >
          ${this._buttonContent(this._stateObj, configState, buttonColor)}
          <mwc-ripple id="ripple"></mwc-ripple>
        </ha-card>
        ${this._getLock(lockStyle)}
      </div>
      ${this._config?.tooltip
        ? html`
            <span class="tooltiptext" style=${styleMap(tooltipStyleFromConfig)}>
              ${this._getTemplateOrValue(this._stateObj, this._config.tooltip)}
            </span>
          `
        : ''}
    `;
  }

  private _getLock(lockStyle: StyleInfo): TemplateResult {
    if (this._config!.lock && this._getTemplateOrValue(this._stateObj, this._config!.lock.enabled)) {
      return html`
        <div
          id="overlay"
          style=${styleMap(lockStyle)}
          @action=${this._handleUnlockType}
          .actionHandler=${actionHandler({
            hasDoubleClick: this._config!.lock!.unlock === 'double_tap',
            hasHold: this._config!.lock!.unlock === 'hold',
          })}
          .config="${this._config}"
        >
          <ha-icon id="lock" icon="mdi:lock-outline"></ha-icon>
        </div>
      `;
    }
    return html``;
  }

  private _buttonContent(
    state: HassEntity | undefined,
    configState: StateConfig | undefined,
    color: string,
  ): TemplateResult {
    const name = this._buildName(state, configState);
    const stateDisplayActual = configState?.state_display || this._config!.state_display || undefined;
    const stateDisplay =
      this._config!.show_state && stateDisplayActual ? this._getTemplateOrValue(state, stateDisplayActual) : undefined;
    const stateString = stateDisplay ? stateDisplay : this._buildStateString(state);
    const nameStateString = buildNameStateConcat(name, stateString);

    switch (this._config!.layout) {
      case 'icon_name_state':
      case 'name_state':
        return this._gridHtml(state, configState, this._config!.layout, color, nameStateString, undefined);
      default:
        return this._gridHtml(state, configState, this._config!.layout, color, name, stateString);
    }
  }

  private _unsafeHTMLorNot(input: any): any {
    if (input.strings || input.values) {
      return input;
    } else {
      return unsafeHTML(`${input}`);
    }
  }

  private _gridHtml(
    state: HassEntity | undefined,
    configState: StateConfig | undefined,
    containerClass: string,
    color: string,
    name: string | undefined,
    stateString: string | undefined,
  ): TemplateResult {
    const iconTemplate = this._getIconHtml(state, configState, color);
    const itemClass: string[] = [containerClass];
    const label = this._buildLabel(state, configState);
    const nameStyleFromConfig = this._buildStyleGeneric(state, configState, 'name');
    const stateStyleFromConfig = this._buildStyleGeneric(state, configState, 'state');
    const labelStyleFromConfig = this._buildStyleGeneric(state, configState, 'label');
    const lastChangedTemplate = this._buildLastChanged(state, labelStyleFromConfig);
    const gridStyleFromConfig = this._buildStyleGeneric(state, configState, 'grid');
    if (!iconTemplate) itemClass.push('no-icon');
    if (!name) itemClass.push('no-name');
    if (!stateString) itemClass.push('no-state');
    if (!label && !lastChangedTemplate) itemClass.push('no-label');

    return html`
      <div id="container" class=${itemClass.join(' ')} style=${styleMap(gridStyleFromConfig)}>
        ${iconTemplate ? iconTemplate : ''}
        ${name
          ? html`
              <div id="name" class="ellipsis" style=${styleMap(nameStyleFromConfig)}>
                ${this._unsafeHTMLorNot(name)}
              </div>
            `
          : ''}
        ${stateString
          ? html`
              <div id="state" class="ellipsis" style=${styleMap(stateStyleFromConfig)}>
                ${this._unsafeHTMLorNot(stateString)}
              </div>
            `
          : ''}
        ${label && !lastChangedTemplate
          ? html`
              <div id="label" class="ellipsis" style=${styleMap(labelStyleFromConfig)}>
                ${this._unsafeHTMLorNot(label)}
              </div>
            `
          : ''}
        ${lastChangedTemplate ? lastChangedTemplate : ''} ${this._buildCustomFields(state, configState)}
      </div>
    `;
  }

  private _getIconHtml(
    state: HassEntity | undefined,
    configState: StateConfig | undefined,
    color: string,
  ): TemplateResult | undefined {
    const icon = this._buildIcon(state, configState);
    const entityPicture = this._buildEntityPicture(state, configState);
    const entityPictureStyleFromConfig = this._buildStyleGeneric(state, configState, 'entity_picture');
    const haIconStyleFromConfig = this._buildStyleGeneric(state, configState, 'icon');
    const imgCellStyleFromConfig = this._buildStyleGeneric(state, configState, 'img_cell');
    const haCardStyleFromConfig = this._buildStyleGeneric(state, configState, 'card');

    const haIconStyle: StyleInfo = {
      color,
      width: this._config!.size,
      '--ha-icon-display': haCardStyleFromConfig.height ? 'inline' : undefined,
      position: !this._config!.aspect_ratio && !haCardStyleFromConfig.height ? 'relative' : 'absolute',
      ...haIconStyleFromConfig,
    };
    const entityPictureStyle: StyleInfo = {
      ...haIconStyle,
      ...entityPictureStyleFromConfig,
    };
    const liveStream = this._buildLiveStream(entityPictureStyle);
    const shouldShowIcon = this._config!.show_icon && (icon || state);

    if (shouldShowIcon || entityPicture) {
      let domain: string | undefined = undefined;
      if (state) {
        domain = computeStateDomain(state);
      }
      return html`
        <div id="img-cell" style=${styleMap(imgCellStyleFromConfig)}>
          ${shouldShowIcon && !entityPicture && !liveStream
            ? html`
                <ha-state-icon
                  .state=${state}
                  ?data-domain=${domain}
                  data-state=${ifDefined(state?.state)}
                  style=${styleMap(haIconStyle)}
                  .icon="${icon}"
                  id="icon"
                  ?rotating=${this._rotate(configState)}
                ></ha-state-icon>
              `
            : ''}
          ${liveStream ? liveStream : ''}
          ${entityPicture && !liveStream
            ? html`
                <img
                  src="${entityPicture}"
                  style=${styleMap(entityPictureStyle)}
                  id="icon"
                  ?rotating=${this._rotate(configState)}
                />
              `
            : ''}
        </div>
      `;
    } else {
      return undefined;
    }
  }

  private _buildLiveStream(style: StyleInfo): TemplateResult | undefined {
    if (this._config!.show_live_stream && this._config!.entity && computeDomain(this._config!.entity) === 'camera') {
      return html`
        <hui-image
          .hass=${this._hass}
          .cameraImage=${this._config!.entity}
          .entity=${this._config!.entity}
          cameraView="live"
          style=${styleMap(style)}
        ></hui-image>
      `;
    } else {
      return undefined;
    }
  }

  private _configFromLLTemplates(ll: any, config: any): ExternalButtonCardConfig {
    const tpl = config.template;
    if (!tpl) return config;
    let result: any = {};
    let mergedStateConfig: StateConfig[] | undefined;
    const tpls = tpl && Array.isArray(tpl) ? tpl : [tpl];
    tpls?.forEach((template) => {
      if (!ll.config.button_card_templates?.[template])
        throw new Error(`Button-card template '${template}' is missing!`);
      const res = this._configFromLLTemplates(ll, ll.config.button_card_templates[template]);
      result = mergeDeep(result, res);
      mergedStateConfig = mergeStatesById(mergedStateConfig, res.state);
    });
    result = mergeDeep(result, config);
    result.state = mergeStatesById(mergedStateConfig, config.state);
    return result as ExternalButtonCardConfig;
  }

  public setConfig(config: ExternalButtonCardConfig): void {
    if (!config) {
      throw new Error('Invalid configuration');
    }
    if (this._initialSetupComplete) {
      this._initialSetupComplete = false;
    }

    this._cards = {};
    this._cardsConfig = {};
    const ll = getLovelace() || getLovelaceCast();
    let template: ExternalButtonCardConfig = copy(config);
    template = this._configFromLLTemplates(ll, template);
    this._config = {
      type: 'custom:button-card',
      group_expand: false,
      hold_action: { action: 'none' },
      double_tap_action: { action: 'none' },
      layout: 'vertical',
      size: '40%',
      color_type: 'icon',
      show_name: true,
      show_state: false,
      show_icon: true,
      show_units: true,
      show_label: false,
      show_entity_picture: false,
      show_live_stream: false,
      card_size: 3,
      ...template,
      lock: {
        enabled: false,
        duration: 5,
        unlock: 'tap',
        ...template.lock,
      },
    };

    if (!this._initialSetupComplete) {
      this._finishSetup();
    }
  }

  private _loopGroup(entityList: string[] | undefined): void {
    if (entityList) {
      entityList.forEach((childEntity) => {
        if (this._hass?.states[childEntity]) {
          if (this._hass.states[childEntity].attributes?.entity_id) {
            this._loopGroup(this._hass.states[childEntity].attributes.entity_id);
          } else {
            if (!this._entities.includes(childEntity)) {
              this._entities.push(childEntity);
            }
          }
        }
      });
    }
  }

  private _expandTriggerGroups(): void {
    if (this._hass && this._config?.group_expand && this._entities) {
      this._entities.forEach((entity) => {
        if (this._hass?.states[entity]?.attributes?.entity_id) {
          this._loopGroup(this._hass?.states[entity].attributes?.entity_id);
        }
      });
    }
  }

  // The height of your card. Home Assistant uses this to automatically
  // distribute all cards over the available columns.
  public getCardSize(): number {
    return this._config?.card_size || 3;
  }

  private _evalActions(config: ButtonCardConfig, action: string): ButtonCardConfig {
    const configDuplicate = copy(config);
    /* eslint no-param-reassign: 0 */
    const __evalObject = (configEval: any): any => {
      if (!configEval) {
        return configEval;
      }
      Object.keys(configEval).forEach((key) => {
        if (typeof configEval[key] === 'object') {
          configEval[key] = __evalObject(configEval[key]);
        } else {
          configEval[key] = this._getTemplateOrValue(this._stateObj, configEval[key]);
        }
      });
      return configEval;
    };
    if (configDuplicate[action]?.service_data?.entity_id === 'entity') {
      configDuplicate[action].service_data.entity_id = config.entity;
    }
    if (configDuplicate[action]?.data?.entity_id === 'entity') {
      configDuplicate[action].data.entity_id = config.entity;
    }
    configDuplicate[action] = __evalObject(configDuplicate[action]);
    if (!configDuplicate[action].confirmation && configDuplicate.confirmation) {
      configDuplicate[action].confirmation = __evalObject(configDuplicate.confirmation);
    }
    if (configDuplicate[action]?.entity) {
      configDuplicate.entity = configDuplicate[action].entity;
    }

    return configDuplicate;
  }

  private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
    // this._shouldRenderRipple = true;
    return this._ripple;
  });

  // backward compatibility
  @eventOptions({ passive: true })
  private handleRippleActivate(evt?: Event): void {
    this._ripple.then((r) => r && typeof r.startPress === 'function' && this._rippleHandlers.startPress(evt));
  }

  private handleRippleDeactivate(): void {
    this._ripple.then((r) => r && typeof r.endPress === 'function' && this._rippleHandlers.endPress());
  }

  private handleRippleFocus(): void {
    this._ripple.then((r) => r && typeof r.startFocus === 'function' && this._rippleHandlers.startFocus());
  }

  private handleRippleBlur(): void {
    this._ripple.then((r) => r && typeof r.endFocus === 'function' && this._rippleHandlers.endFocus());
  }

  private _handleAction(ev: any): void {
    if (ev.detail?.action) {
      switch (ev.detail.action) {
        case 'tap':
        case 'hold':
        case 'double_tap':
          const config = this._config;
          if (!config) return;
          const action = ev.detail.action;
          const localAction = this._evalActions(config, `${action}_action`);
          handleAction(this, this._hass!, localAction, action);
          break;
        default:
          break;
      }
    }
  }

  private _handleUnlockType(ev): void {
    const config = this._config as ButtonCardConfig;
    if (!config) return;
    if (config.lock.unlock === ev.detail.action) {
      this._handleLock();
    }
  }

  private _handleLock(): void {
    const lock = this.shadowRoot!.getElementById('lock') as LitElement;
    if (!lock) return;
    if (this._config!.lock!.exemptions) {
      if (!this._hass!.user?.name || !this._hass!.user.id) return;
      let matched = false;
      this._config!.lock!.exemptions.forEach((e) => {
        if (
          (!matched && (e as ExemptionUserConfig).user === this._hass!.user?.id) ||
          (e as ExemptionUsernameConfig).username === this._hass!.user?.name
        ) {
          matched = true;
        }
      });
      if (!matched) {
        lock.classList.add('invalid');
        window.setTimeout(() => {
          if (lock) {
            lock.classList.remove('invalid');
          }
        }, 3000);
        return;
      }
    }
    const overlay = this.shadowRoot!.getElementById('overlay') as LitElement;
    overlay.style.setProperty('pointer-events', 'none');

    if (lock) {
      const icon = document.createAttribute('icon');
      icon.value = 'mdi:lock-open-outline';
      lock.attributes.setNamedItem(icon);
      lock.classList.add('hidden');
    }
    window.setTimeout(() => {
      overlay.style.setProperty('pointer-events', '');
      if (lock) {
        lock.classList.remove('hidden');
        const icon = document.createAttribute('icon');
        icon.value = 'mdi:lock-outline';
        lock.attributes.setNamedItem(icon);
      }
    }, this._config!.lock!.duration! * 1000);
  }

  private _stopPropagation(ev: Event): void {
    ev.stopPropagation();
  }
}
