///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///

import {
  AfterViewInit,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ComponentFactoryResolver,
  ComponentRef,
  ElementRef,
  Inject,
  Injector,
  Input,
  NgZone,
  OnChanges,
  OnDestroy,
  OnInit, Renderer2,
  SimpleChanges,
  ViewChild,
  ViewContainerRef,
  ViewEncapsulation
} from '@angular/core';
import { DashboardWidget } from '@home/models/dashboard-component.models';
import {
  defaultLegendConfig,
  LegendConfig,
  LegendData,
  LegendPosition, MobileActionResult,
  Widget,
  WidgetActionDescriptor,
  widgetActionSources,
  WidgetActionType,
  WidgetComparisonSettings, WidgetMobileActionDescriptor, WidgetMobileActionType,
  WidgetResource,
  widgetType,
  WidgetTypeParameters
} from '@shared/models/widget.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service';
import { UtilsService } from '@core/services/utils.service';
import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs';
import {
  deepClone,
  insertVariable,
  isDefined,
  isNotEmptyStr,
  objToBase64,
  objToBase64URI,
  validateEntityId
} from '@core/utils';
import {
  IDynamicWidgetComponent, ShowWidgetHeaderActionFunction, updateEntityParams,
  WidgetContext,
  WidgetHeaderAction,
  WidgetInfo,
  WidgetTypeInstance
} from '@home/models/widget-component.models';
import {
  IWidgetSubscription,
  StateObject,
  StateParams,
  SubscriptionEntityInfo,
  SubscriptionInfo,
  SubscriptionMessage,
  WidgetSubscriptionContext,
  WidgetSubscriptionOptions
} from '@core/api/widget-api.models';
import { EntityId } from '@shared/models/id/entity-id';
import { ActivatedRoute, Router } from '@angular/router';
import cssjs from '@core/css/css';
import { ResourcesService } from '@core/services/resources.service';
import { catchError, map, switchMap } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TimeService } from '@core/services/time.service';
import { DeviceService } from '@app/core/http/device.service';
import { ExceptionData } from '@shared/models/error.models';
import { WidgetComponentService } from './widget-component.service';
import { Timewindow } from '@shared/models/time/time.models';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { DashboardService } from '@core/http/dashboard.service';
import { WidgetSubscription } from '@core/api/widget-subscription';
import { EntityService } from '@core/http/entity.service';
import { ServicesMap } from '@home/models/services.map';
import { ResizeObserver } from '@juggle/resize-observer';
import { EntityDataService } from '@core/api/entity-data.service';
import { TranslateService } from '@ngx-translate/core';
import { NotificationType } from '@core/notification/notification.models';
import { AlarmDataService } from '@core/api/alarm-data.service';
import { MatDialog } from '@angular/material/dialog';
import { ComponentType } from '@angular/cdk/portal';
import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token';
import { MobileService } from '@core/services/mobile.service';
import { DialogService } from '@core/services/dialog.service';
import { PopoverPlacement } from '@shared/components/popover.models';
import { TbPopoverService } from '@shared/components/popover.service';
import {
  DASHBOARD_PAGE_COMPONENT_TOKEN
} from '@home/components/tokens';

@Component({
  selector: 'tb-widget',
  templateUrl: './widget.component.html',
  styleUrls: ['./widget.component.scss'],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class WidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {

  @Input()
  isEdit: boolean;

  @Input()
  isMobile: boolean;

  @Input()
  dashboardWidget: DashboardWidget;

  @ViewChild('widgetContent', {read: ViewContainerRef, static: true}) widgetContentContainer: ViewContainerRef;

  widget: Widget;
  widgetInfo: WidgetInfo;
  errorMessages: string[];
  widgetContext: WidgetContext;
  widgetType: any;
  typeParameters: WidgetTypeParameters;
  widgetTypeInstance: WidgetTypeInstance;
  widgetErrorData: ExceptionData;
  loadingData: boolean;
  displayNoData = false;
  noDataDisplayMessageText: string;

  displayLegend: boolean;
  legendConfig: LegendConfig;
  legendData: LegendData;
  isLegendFirst: boolean;
  legendContainerLayoutType: string;
  legendStyle: {[klass: string]: any};

  dynamicWidgetComponentRef: ComponentRef<IDynamicWidgetComponent>;
  dynamicWidgetComponent: IDynamicWidgetComponent;

  subscriptionContext: WidgetSubscriptionContext;

  subscriptionInited = false;
  destroyed = false;
  widgetSizeDetected = false;
  widgetInstanceInited = false;
  dataUpdatePending = false;
  latestDataUpdatePending = false;
  pendingMessage: SubscriptionMessage;

  cafs: {[cafId: string]: CancelAnimationFrame} = {};

  toastTargetId = 'widget-messages-' + this.utils.guid();

  private widgetResize$: ResizeObserver;

  private cssParser = new cssjs();

  private rxSubscriptions = new Array<Subscription>();

  constructor(protected store: Store<AppState>,
              private route: ActivatedRoute,
              private router: Router,
              private widgetComponentService: WidgetComponentService,
              private componentFactoryResolver: ComponentFactoryResolver,
              private elementRef: ElementRef,
              private injector: Injector,
              private dialog: MatDialog,
              private renderer: Renderer2,
              private popoverService: TbPopoverService,
              @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType<any>,
              @Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType<any>,
              private widgetService: WidgetService,
              private resources: ResourcesService,
              private timeService: TimeService,
              private deviceService: DeviceService,
              private entityService: EntityService,
              private dashboardService: DashboardService,
              private entityDataService: EntityDataService,
              private alarmDataService: AlarmDataService,
              private translate: TranslateService,
              private utils: UtilsService,
              private mobileService: MobileService,
              private dialogs: DialogService,
              private raf: RafService,
              private ngZone: NgZone,
              private cd: ChangeDetectorRef) {
    super(store);
    this.cssParser.testMode = false;
  }

  ngOnInit(): void {

    this.loadingData = true;

    this.widget = this.dashboardWidget.widget;

    this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend
      : this.widget.type === widgetType.timeseries;

    this.legendContainerLayoutType = 'column';

    if (this.displayLegend) {
      this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type);
      this.legendData = {
        keys: [],
        data: []
      };
      if (this.legendConfig.position === LegendPosition.top ||
        this.legendConfig.position === LegendPosition.bottom) {
        this.legendContainerLayoutType = 'column';
        this.isLegendFirst = this.legendConfig.position === LegendPosition.top;
      } else {
        this.legendContainerLayoutType = 'row';
        this.isLegendFirst = this.legendConfig.position === LegendPosition.left;
      }
      switch (this.legendConfig.position) {
        case LegendPosition.top:
          this.legendStyle = {
            paddingBottom: '8px',
            maxHeight: '50%',
            overflowY: 'auto'
          };
          break;
        case LegendPosition.bottom:
          this.legendStyle = {
            paddingTop: '8px',
            maxHeight: '50%',
            overflowY: 'auto'
          };
          break;
        case LegendPosition.left:
          this.legendStyle = {
            paddingRight: '0px',
            maxWidth: '50%',
            overflowY: 'auto'
          };
          break;
        case LegendPosition.right:
          this.legendStyle = {
            paddingLeft: '0px',
            maxWidth: '50%',
            overflowY: 'auto'
          };
          break;
      }
    }

    const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {};
    if (this.widget.config.actions) {
      for (const actionSourceId of Object.keys(this.widget.config.actions)) {
        const descriptors = this.widget.config.actions[actionSourceId];
        const actionDescriptors: Array<WidgetActionDescriptor> = [];
        descriptors.forEach((descriptor) => {
          const actionDescriptor: WidgetActionDescriptor = deepClone(descriptor);
          actionDescriptor.displayName = this.utils.customTranslation(descriptor.name, descriptor.name);
          actionDescriptors.push(actionDescriptor);
        });
        actionDescriptorsBySourceId[actionSourceId] = actionDescriptors;
      }
    }

    this.widgetContext = this.dashboardWidget.widgetContext;
    this.widgetContext.changeDetector = this.cd;
    this.widgetContext.ngZone = this.ngZone;
    this.widgetContext.store = this.store;
    this.widgetContext.servicesMap = ServicesMap;
    this.widgetContext.isEdit = this.isEdit;
    this.widgetContext.isMobile = this.isMobile;
    this.widgetContext.toastTargetId = this.toastTargetId;

    this.widgetContext.subscriptionApi = {
      createSubscription: this.createSubscription.bind(this),
      createSubscriptionFromInfo: this.createSubscriptionFromInfo.bind(this),
      removeSubscription: (id) => {
        const subscription = this.widgetContext.subscriptions[id];
        if (subscription) {
          subscription.destroy();
          delete this.widgetContext.subscriptions[id];
        }
      }
    };

    this.widgetContext.actionsApi = {
      actionDescriptorsBySourceId,
      getActionDescriptors: this.getActionDescriptors.bind(this),
      handleWidgetAction: this.handleWidgetAction.bind(this),
      elementClick: this.elementClick.bind(this),
      getActiveEntityInfo: this.getActiveEntityInfo.bind(this),
      openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this),
      openDashboardStateInPopover: this.openDashboardStateInPopover.bind(this)
    };

    this.widgetContext.customHeaderActions = [];
    const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value);
    headerActionsDescriptors.forEach((descriptor) =>
    {
      let useShowWidgetHeaderActionFunction = descriptor.useShowWidgetActionFunction || false;
      let showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction = null;
      if (useShowWidgetHeaderActionFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) {
        try {
          showWidgetHeaderActionFunction =
            new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowWidgetHeaderActionFunction;
        } catch (e) {
          useShowWidgetHeaderActionFunction = false;
        }
      }
      const headerAction: WidgetHeaderAction = {
        name: descriptor.name,
        displayName: descriptor.displayName,
        icon: descriptor.icon,
        descriptor,
        useShowWidgetHeaderActionFunction,
        showWidgetHeaderActionFunction,
        onAction: $event => {
          const entityInfo = this.getActiveEntityInfo();
          const entityId = entityInfo ? entityInfo.entityId : null;
          const entityName = entityInfo ? entityInfo.entityName : null;
          const entityLabel = entityInfo ? entityInfo.entityLabel : null;
          this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);
        }
      };
      this.widgetContext.customHeaderActions.push(headerAction);
    });

    this.subscriptionContext = new WidgetSubscriptionContext(this.widgetContext.dashboard);
    this.subscriptionContext.timeService = this.timeService;
    this.subscriptionContext.deviceService = this.deviceService;
    this.subscriptionContext.translate = this.translate;
    this.subscriptionContext.entityDataService = this.entityDataService;
    this.subscriptionContext.alarmDataService = this.alarmDataService;
    this.subscriptionContext.utils = this.utils;
    this.subscriptionContext.raf = this.raf;
    this.subscriptionContext.widgetUtils = this.widgetContext.utils;
    this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService);

    this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe(
      (widgetInfo) => {
        this.widgetInfo = widgetInfo;
        this.loadFromWidgetInfo();
      },
      (errorData) => {
        this.widgetInfo = errorData.widgetInfo;
        this.errorMessages = errorData.errorMessages;
        this.loadFromWidgetInfo();
      }
    );
    setTimeout(() => {
      this.dashboardWidget.updateWidgetParams();
    }, 0);

    const noDataDisplayMessage = this.widget.config.noDataDisplayMessage;
    if (isNotEmptyStr(noDataDisplayMessage)) {
      this.noDataDisplayMessageText = this.utils.customTranslation(noDataDisplayMessage, noDataDisplayMessage);
    } else {
      this.noDataDisplayMessageText = this.translate.instant('widget.no-data');
    }
  }

  ngAfterViewInit(): void {
  }

  ngOnChanges(changes: SimpleChanges): void {
    for (const propName of Object.keys(changes)) {
      const change = changes[propName];
      if (!change.firstChange && change.currentValue !== change.previousValue) {
        if (propName === 'isEdit') {
          this.onEditModeChanged();
        } else if (propName === 'isMobile') {
          this.onMobileModeChanged();
        }
      }
    }
  }

  ngOnDestroy(): void {
    this.destroyed = true;
    this.rxSubscriptions.forEach((subscription) => {
      subscription.unsubscribe();
    });
    this.rxSubscriptions.length = 0;
    this.onDestroy();
  }

  private displayWidgetInstance(): boolean {
    if (this.widget.type !== widgetType.static) {
      for (const id of Object.keys(this.widgetContext.subscriptions)) {
        const subscription = this.widgetContext.subscriptions[id];
        if (subscription.isDataResolved()) {
          return true;
        }
      }
      return false;
    } else {
      return true;
    }
  }

  private onDestroy() {
    if (this.widgetContext) {
      const shouldDestroyWidgetInstance = this.displayWidgetInstance();
      for (const id of Object.keys(this.widgetContext.subscriptions)) {
        const subscription = this.widgetContext.subscriptions[id];
        subscription.destroy();
      }
      this.subscriptionInited = false;
      this.dataUpdatePending = false;
      this.pendingMessage = null;
      this.widgetContext.subscriptions = {};
      if (this.widgetContext.inited) {
        this.widgetContext.inited = false;
        for (const cafId of Object.keys(this.cafs)) {
          if (this.cafs[cafId]) {
            this.cafs[cafId]();
            this.cafs[cafId] = null;
          }
        }
        try {
          if (shouldDestroyWidgetInstance) {
            this.widgetTypeInstance.onDestroy();
            this.widgetInstanceInited = false;
          }
        } catch (e) {
          this.handleWidgetException(e);
        }
      }
      this.widgetContext.destroyed = true;
      this.destroyDynamicWidgetComponent();
    }
  }

  public onTimewindowChanged(timewindow: Timewindow) {
    for (const id of Object.keys(this.widgetContext.subscriptions)) {
      const subscription = this.widgetContext.subscriptions[id];
      if (!subscription.useDashboardTimewindow) {
        subscription.updateTimewindowConfig(timewindow);
      }
    }
  }

  public onLegendKeyHiddenChange(index: number) {
    for (const id of Object.keys(this.widgetContext.subscriptions)) {
      const subscription = this.widgetContext.subscriptions[id];
      subscription.updateDataVisibility(index);
    }
  }

  private loadFromWidgetInfo() {
    this.widgetContext.widgetNamespace = `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`;
    const elem = this.elementRef.nativeElement;
    elem.classList.add('tb-widget');
    elem.classList.add(this.widgetContext.widgetNamespace);
    this.widgetType = this.widgetInfo.widgetTypeFunction;
    this.typeParameters = this.widgetInfo.typeParameters;

    if (!this.widgetType) {
      this.widgetTypeInstance = {};
    } else {
      try {
        this.widgetTypeInstance = new this.widgetType(this.widgetContext);
      } catch (e) {
        this.handleWidgetException(e);
        this.widgetTypeInstance = {};
      }
    }
    if (!this.widgetTypeInstance.onInit) {
      this.widgetTypeInstance.onInit = () => {};
    }
    if (!this.widgetTypeInstance.onDataUpdated) {
      this.widgetTypeInstance.onDataUpdated = () => {};
    }
    if (!this.widgetTypeInstance.onLatestDataUpdated) {
      this.widgetTypeInstance.onLatestDataUpdated = () => {};
    }
    if (!this.widgetTypeInstance.onResize) {
      this.widgetTypeInstance.onResize = () => {};
    }
    if (!this.widgetTypeInstance.onEditModeChanged) {
      this.widgetTypeInstance.onEditModeChanged = () => {};
    }
    if (!this.widgetTypeInstance.onMobileModeChanged) {
      this.widgetTypeInstance.onMobileModeChanged = () => {};
    }
    if (!this.widgetTypeInstance.onDestroy) {
      this.widgetTypeInstance.onDestroy = () => {};
    }

    this.initialize().subscribe(
      () => {
        this.onInit();
      },
      (err) => {
        // console.log(err);
      }
    );
  }

  private detectChanges(detectContainerChanges = false) {
    if (!this.destroyed) {
      try {
        this.cd.detectChanges();
        if (detectContainerChanges) {
          this.widgetContext.detectContainerChanges();
        }
      } catch (e) {
        // console.log(e);
      }
    }
  }

  private isReady(): boolean {
    return this.subscriptionInited && this.widgetSizeDetected;
  }

  private onInit(skipSizeCheck?: boolean) {
    if (!this.widgetContext.$containerParent || this.destroyed) {
      return;
    }
    if (!skipSizeCheck) {
      this.checkSize();
    }
    if (!this.widgetContext.inited && this.isReady()) {
      this.widgetContext.inited = true;
      this.widgetContext.detectContainerChanges();
      if (this.cafs.init) {
        this.cafs.init();
        this.cafs.init = null;
      }
      this.cafs.init = this.raf.raf(() => {
        try {
          if (this.displayWidgetInstance()) {
            this.widgetTypeInstance.onInit();
            this.widgetInstanceInited = true;
            if (this.dataUpdatePending) {
              this.widgetTypeInstance.onDataUpdated();
              setTimeout(() => {
                this.dashboardWidget.updateCustomHeaderActions(true);
              }, 0);
              this.dataUpdatePending = false;
            }
            if (this.latestDataUpdatePending) {
              this.widgetTypeInstance.onLatestDataUpdated();
              this.latestDataUpdatePending = false;
            }
            if (this.pendingMessage) {
              this.displayMessage(this.pendingMessage.severity, this.pendingMessage.message);
              this.pendingMessage = null;
            }
          } else {
            this.loadingData = false;
            this.displayNoData = true;
          }
          this.detectChanges();
        } catch (e) {
          this.handleWidgetException(e);
        }
      });
      if (!this.typeParameters.useCustomDatasources && this.widgetContext.defaultSubscription) {
        this.widgetContext.defaultSubscription.subscribe();
      }
    }
  }

  private onResize() {
    if (this.checkSize()) {
      if (this.widgetContext.inited) {
        if (this.cafs.resize) {
          this.cafs.resize();
          this.cafs.resize = null;
        }
        this.cafs.resize = this.raf.raf(() => {
          try {
            if (this.displayWidgetInstance()) {
              this.widgetTypeInstance.onResize();
            }
          } catch (e) {
            this.handleWidgetException(e);
          }
        });
      } else {
        this.onInit(true);
      }
    }
  }

  private onEditModeChanged() {
    if (this.widgetContext.isEdit !== this.isEdit) {
      this.widgetContext.isEdit = this.isEdit;
      if (this.widgetContext.inited) {
        if (this.cafs.editMode) {
          this.cafs.editMode();
          this.cafs.editMode = null;
        }
        this.cafs.editMode = this.raf.raf(() => {
          try {
            if (this.displayWidgetInstance()) {
              this.widgetTypeInstance.onEditModeChanged();
            }
          } catch (e) {
            this.handleWidgetException(e);
          }
        });
      }
    }
  }

  private onMobileModeChanged() {
    if (this.widgetContext.isMobile !== this.isMobile) {
      this.widgetContext.isMobile = this.isMobile;
      if (this.widgetContext.inited) {
        if (this.cafs.mobileMode) {
          this.cafs.mobileMode();
          this.cafs.mobileMode = null;
        }
        this.cafs.mobileMode = this.raf.raf(() => {
          try {
            if (this.displayWidgetInstance()) {
              this.widgetTypeInstance.onMobileModeChanged();
            }
          } catch (e) {
            this.handleWidgetException(e);
          }
        });
      }
    }
  }

  private reInit() {
    if (this.cafs.reinit) {
      this.cafs.reinit();
      this.cafs.reinit = null;
    }
    this.cafs.reinit = this.raf.raf(() => {
      this.ngZone.run(() => {
        this.reInitImpl();
      });
    });
  }

  private reInitImpl() {
    this.onDestroy();
    if (!this.typeParameters.useCustomDatasources) {
      this.createDefaultSubscription().subscribe(
        () => {
          if (this.destroyed) {
            this.onDestroy();
          } else {
            this.widgetContext.reset();
            this.subscriptionInited = true;
            this.configureDynamicWidgetComponent();
            this.onInit();
          }
        },
        () => {
          if (this.destroyed) {
            this.onDestroy();
          } else {
            this.widgetContext.reset();
            this.subscriptionInited = true;
            this.onInit();
          }
        }
      );
    } else {
      this.widgetContext.reset();
      this.subscriptionInited = true;
      this.configureDynamicWidgetComponent();
      this.onInit();
    }
  }

  private initialize(): Observable<any> {

    const initSubject = new ReplaySubject();

    this.rxSubscriptions.push(this.widgetContext.aliasController.entityAliasesChanged.subscribe(
      (aliasIds) => {
        let subscriptionChanged = false;
        for (const id of Object.keys(this.widgetContext.subscriptions)) {
          const subscription = this.widgetContext.subscriptions[id];
          subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds);
        }
        if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {
          this.displayNoData = false;
          this.reInit();
        }
      }
    ));

    this.rxSubscriptions.push(this.widgetContext.aliasController.filtersChanged.subscribe(
      (filterIds) => {
        let subscriptionChanged = false;
        for (const id of Object.keys(this.widgetContext.subscriptions)) {
          const subscription = this.widgetContext.subscriptions[id];
          subscriptionChanged = subscriptionChanged || subscription.onFiltersChanged(filterIds);
        }
        if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {
          this.displayNoData = false;
          this.reInit();
        }
      }
    ));

    this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe(
      (dashboardTimewindow) => {
        for (const id of Object.keys(this.widgetContext.subscriptions)) {
          const subscription = this.widgetContext.subscriptions[id];
          subscription.onDashboardTimewindowChanged(dashboardTimewindow);
        }
      }
    ));
    if (!this.typeParameters.useCustomDatasources) {
      this.createDefaultSubscription().subscribe(
        () => {
          this.subscriptionInited = true;
          this.configureDynamicWidgetComponent();
          initSubject.next();
          initSubject.complete();
        },
        (err) => {
          this.subscriptionInited = true;
          initSubject.error(err);
        }
      );
    } else {
      this.loadingData = false;
      this.subscriptionInited = true;
      this.configureDynamicWidgetComponent();
      initSubject.next();
      initSubject.complete();
    }
    return initSubject.asObservable();
  }

  private destroyDynamicWidgetComponent() {
    if (this.widgetContext.$containerParent && this.widgetResize$) {
      this.widgetResize$.disconnect();
    }
    if (this.dynamicWidgetComponentRef) {
      this.dynamicWidgetComponentRef.destroy();
      this.dynamicWidgetComponentRef = null;
    }
  }

  private handleWidgetException(e) {
    console.error(e);
    this.widgetErrorData = this.utils.processWidgetException(e);
    this.detectChanges();
  }

  private displayMessage(type: NotificationType, message: string, duration?: number) {
    this.widgetContext.showToast(type, message, duration, 'bottom', 'right', this.toastTargetId);
  }

  private clearMessage() {
    this.widgetContext.hideToast(this.toastTargetId);
  }

  private configureDynamicWidgetComponent() {
      this.widgetContentContainer.clear();
      const injector: Injector = Injector.create(
        {
          providers: [
            {
              provide: 'widgetContext',
              useValue: this.widgetContext
            },
            {
              provide: 'errorMessages',
              useValue: this.errorMessages
            }
          ],
          parent: this.injector
        }
      );

      const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container'));
      this.widgetContext.$containerParent = $(containerElement);

      try {
        this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector);
        this.cd.detectChanges();
      } catch (e) {
        console.error(e);
        if (this.dynamicWidgetComponentRef) {
          this.dynamicWidgetComponentRef.destroy();
          this.dynamicWidgetComponentRef = null;
        }
        this.widgetContentContainer.clear();
      }

      if (this.dynamicWidgetComponentRef) {
        this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance;
        this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement);
        this.widgetContext.$container.css('display', 'block');
        this.widgetContext.$container.attr('id', 'container');
        if (this.widgetSizeDetected) {
          this.widgetContext.$container.css('height', this.widgetContext.height + 'px');
          this.widgetContext.$container.css('width', this.widgetContext.width + 'px');
        }
      }

      this.widgetResize$ = new ResizeObserver(() => {
        this.onResize();
      });
      this.widgetResize$.observe(this.widgetContext.$containerParent[0]);
  }

  private createSubscription(options: WidgetSubscriptionOptions, subscribe?: boolean): Observable<IWidgetSubscription> {
    const createSubscriptionSubject = new ReplaySubject<IWidgetSubscription>();
    options.dashboardTimewindow = this.widgetContext.dashboardTimewindow;
    const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options);
    subscription.init$.subscribe(
      () => {
        this.widgetContext.subscriptions[subscription.id] = subscription;
        if (subscribe) {
          subscription.subscribe();
        }
        createSubscriptionSubject.next(subscription);
        createSubscriptionSubject.complete();
      },
      (err) => {
        createSubscriptionSubject.error(err);
      }
    );
    return createSubscriptionSubject.asObservable();
  }

  private createSubscriptionFromInfo(type: widgetType, subscriptionsInfo: Array<SubscriptionInfo>,
                                     options: WidgetSubscriptionOptions, useDefaultComponents: boolean,
                                     subscribe: boolean): Observable<IWidgetSubscription> {
    const createSubscriptionSubject = new ReplaySubject<IWidgetSubscription>();
    options.type = type;

    if (useDefaultComponents) {
      this.defaultComponentsOptions(options);
    } else {
      if (!options.timeWindowConfig) {
        options.useDashboardTimewindow = true;
      }
    }
    if (options.type === widgetType.alarm) {
      options.alarmSource = this.entityService.createAlarmSourceFromSubscriptionInfo(subscriptionsInfo[0]);
    } else {
      options.datasources = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo);
    }
    this.createSubscription(options, subscribe).subscribe(
      (subscription) => {
        if (useDefaultComponents) {
          this.defaultSubscriptionOptions(subscription, options);
        }
        createSubscriptionSubject.next(subscription);
        createSubscriptionSubject.complete();
      },
      (err) => {
        createSubscriptionSubject.error(err);
      }
    );
    return createSubscriptionSubject.asObservable();
  }

  private defaultComponentsOptions(options: WidgetSubscriptionOptions) {
    options.useDashboardTimewindow = isDefined(this.widget.config.useDashboardTimewindow)
          ? this.widget.config.useDashboardTimewindow : true;
    options.displayTimewindow = isDefined(this.widget.config.displayTimewindow)
      ? this.widget.config.displayTimewindow : !options.useDashboardTimewindow;
    options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow;
    options.legendConfig = null;
    if (this.displayLegend) {
      options.legendConfig = this.legendConfig;
    }
    options.decimals = this.widgetContext.decimals;
    options.units = this.widgetContext.units;
    options.callbacks = {
      onDataUpdated: () => {
        try {
          if (this.displayWidgetInstance()) {
            if (this.widgetInstanceInited) {
              this.widgetTypeInstance.onDataUpdated();
              setTimeout(() => {
                this.dashboardWidget.updateCustomHeaderActions(true);
              }, 0);
            } else {
              this.dataUpdatePending = true;
            }
          }
        } catch (e){}
      },
      onLatestDataUpdated: () => {
        try {
          if (this.displayWidgetInstance()) {
            if (this.widgetInstanceInited) {
              this.widgetTypeInstance.onLatestDataUpdated();
            } else {
              this.latestDataUpdatePending = true;
            }
          }
        } catch (e){}
      },
      onDataUpdateError: (subscription, e) => {
        this.handleWidgetException(e);
      },
      onLatestDataUpdateError: (subscription, e) => {
        this.handleWidgetException(e);
      },
      onSubscriptionMessage: (subscription, message) => {
        if (this.displayWidgetInstance()) {
          if (this.widgetInstanceInited) {
            this.displayMessage(message.severity, message.message);
          } else {
            this.pendingMessage = message;
          }
        }
      },
      onInitialPageDataChanged: (subscription, nextPageData) => {
        this.reInit();
      },
      forceReInit: () => {
        this.reInit();
      },
      dataLoading: (subscription) => {
        if (this.loadingData !== subscription.loadingData) {
          this.loadingData = subscription.loadingData;
          this.detectChanges();
        }
      },
      legendDataUpdated: (subscription, detectChanges) => {
        if (detectChanges) {
          this.detectChanges();
        }
      },
      timeWindowUpdated: (subscription, timeWindowConfig) => {
        this.ngZone.run(() => {
          this.widget.config.timewindow = timeWindowConfig;
          this.detectChanges(true);
        });
      }
    };

  }

  private defaultSubscriptionOptions(subscription: IWidgetSubscription, options: WidgetSubscriptionOptions) {
    if (this.displayLegend) {
      this.legendData = subscription.legendData;
    }
  }

  private createDefaultSubscription(): Observable<any> {
    const createSubscriptionSubject = new ReplaySubject();
    let options: WidgetSubscriptionOptions;
    if (this.widget.type !== widgetType.rpc && this.widget.type !== widgetType.static) {
      const comparisonSettings: WidgetComparisonSettings = this.widgetContext.settings;
      options = {
        type: this.widget.type,
        stateData: this.typeParameters.stateData,
        datasourcesOptional: this.typeParameters.datasourcesOptional,
        hasDataPageLink: this.typeParameters.hasDataPageLink,
        singleEntity: this.typeParameters.singleEntity,
        warnOnPageDataOverflow: this.typeParameters.warnOnPageDataOverflow,
        ignoreDataUpdateOnIntervalTick: this.typeParameters.ignoreDataUpdateOnIntervalTick,
        comparisonEnabled: comparisonSettings.comparisonEnabled,
        timeForComparison: comparisonSettings.timeForComparison,
        comparisonCustomIntervalValue: comparisonSettings.comparisonCustomIntervalValue,
        pageSize: this.widget.config.pageSize
      };
      if (this.widget.type === widgetType.alarm) {
        options.alarmSource = deepClone(this.widget.config.alarmSource);
      } else {
        options.datasources = deepClone(this.widget.config.datasources);
      }

      this.defaultComponentsOptions(options);

      this.createSubscription(options).subscribe(
        (subscription) => {
          this.defaultSubscriptionOptions(subscription, options);

          // backward compatibility
          this.widgetContext.datasources = subscription.datasources;
          this.widgetContext.data = subscription.data;
          this.widgetContext.latestData = subscription.latestData;
          this.widgetContext.hiddenData = subscription.hiddenData;
          this.widgetContext.timeWindow = subscription.timeWindow;
          this.widgetContext.defaultSubscription = subscription;
          createSubscriptionSubject.next();
          createSubscriptionSubject.complete();
        },
        (err) => {
          createSubscriptionSubject.error(err);
        }
      );
    } else if (this.widget.type === widgetType.rpc) {
      this.loadingData = false;
      options = {
        type: this.widget.type,
        targetDeviceAliasIds: this.widget.config.targetDeviceAliasIds
      };
      options.callbacks = {
        rpcStateChanged: (subscription) => {
          if (this.dynamicWidgetComponent) {
            this.dynamicWidgetComponent.rpcEnabled = subscription.rpcEnabled;
            this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
            this.detectChanges();
          }
        },
        onRpcSuccess: (subscription) => {
          if (this.dynamicWidgetComponent) {
            this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
            this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
            this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
            this.clearMessage();
            this.detectChanges();
          }
        },
        onRpcFailed: (subscription) => {
          if (this.dynamicWidgetComponent) {
            this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
            this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
            this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
            if (subscription.rpcErrorText) {
              this.displayMessage('error', subscription.rpcErrorText);
            }
            this.detectChanges();
          }
        },
        onRpcErrorCleared: (subscription) => {
          if (this.dynamicWidgetComponent) {
            this.dynamicWidgetComponent.rpcErrorText = null;
            this.dynamicWidgetComponent.rpcRejection = null;
            this.clearMessage();
            this.detectChanges();
          }
        }
      };
      this.createSubscription(options).subscribe(
        (subscription) => {
          this.widgetContext.defaultSubscription = subscription;
          createSubscriptionSubject.next();
          createSubscriptionSubject.complete();
        },
        (err) => {
          createSubscriptionSubject.error(err);
        }
      );
      this.detectChanges();
    } else if (this.widget.type === widgetType.static) {
      this.loadingData = false;
      createSubscriptionSubject.next();
      createSubscriptionSubject.complete();
      this.detectChanges();
    } else {
      createSubscriptionSubject.next();
      createSubscriptionSubject.complete();
      this.detectChanges();
    }
    return createSubscriptionSubject.asObservable();
  }

  private getActionDescriptors(actionSourceId: string): Array<WidgetActionDescriptor> {
    let result = this.widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId];
    if (!result) {
      result = [];
    }
    return result;
  }

  private handleWidgetAction($event: Event, descriptor: WidgetActionDescriptor,
                             entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string): void {
    const type = descriptor.type;
    const targetEntityParamName = descriptor.stateEntityParamName;
    let targetEntityId: EntityId;
    if (descriptor.setEntityId && validateEntityId(entityId)) {
      targetEntityId = entityId;
    }
    switch (type) {
      case WidgetActionType.openDashboardState:
      case WidgetActionType.updateDashboardState:
        const params = deepClone(this.widgetContext.stateController.getStateParams());
        updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel);
        if (type === WidgetActionType.openDashboardState) {
          if (descriptor.openInPopover) {
            this.openDashboardStateInPopover($event, descriptor.targetDashboardStateId, params,
              descriptor.popoverHideDashboardToolbar, descriptor.popoverPreferredPlacement,
              descriptor.popoverHideOnClickOutside, descriptor.popoverWidth, descriptor.popoverHeight, descriptor.popoverStyle);
          } else if (descriptor.openInSeparateDialog && !this.mobileService.isMobileApp()) {
            this.openDashboardStateInSeparateDialog(descriptor.targetDashboardStateId, params, descriptor.dialogTitle,
              descriptor.dialogHideDashboardToolbar, descriptor.dialogWidth, descriptor.dialogHeight);
          } else {
            this.widgetContext.stateController.openState(descriptor.targetDashboardStateId, params, descriptor.openRightLayout);
          }
        } else {
          this.widgetContext.stateController.updateState(descriptor.targetDashboardStateId, params, descriptor.openRightLayout);
        }
        break;
      case WidgetActionType.openDashboard:
        const targetDashboardId = descriptor.targetDashboardId;
        const stateObject: StateObject = {};
        stateObject.params = {};
        updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel);
        if (descriptor.targetDashboardStateId) {
          stateObject.id = descriptor.targetDashboardStateId;
        }
        const state = objToBase64URI([ stateObject ]);
        const isSinglePage = this.route.snapshot.data.singlePageMode;
        let url;
        if (isSinglePage) {
          url = `/dashboard/${targetDashboardId}?state=${state}`;
        } else {
          url = `/dashboards/${targetDashboardId}?state=${state}`;
        }
        if (descriptor.openNewBrowserTab) {
          window.open(url, '_blank');
        } else {
          this.router.navigateByUrl(url);
        }
        break;
      case WidgetActionType.custom:
        const customFunction = descriptor.customFunction;
        if (customFunction && customFunction.length > 0) {
          try {
            if (!additionalParams) {
              additionalParams = {};
            }
            const customActionFunction = new Function('$event', 'widgetContext', 'entityId',
              'entityName', 'additionalParams', 'entityLabel', customFunction);
            customActionFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
          } catch (e) {
            console.error(e);
          }
        }
        break;
      case WidgetActionType.customPretty:
        const customPrettyFunction = descriptor.customFunction;
        const customHtml = descriptor.customHtml;
        const customCss = descriptor.customCss;
        const customResources = descriptor.customResources;
        const actionNamespace = `custom-action-pretty-${descriptor.name.toLowerCase()}`;
        let htmlTemplate = '';
        if (isDefined(customHtml) && customHtml.length > 0) {
          htmlTemplate = customHtml;
        }
        this.loadCustomActionResources(actionNamespace, customCss, customResources).subscribe(
          () => {
            if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) {
              try {
                if (!additionalParams) {
                  additionalParams = {};
                }
                const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId',
                  'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction);
                customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel);
              } catch (e) {
                console.error(e);
              }
            }
          },
          (errorMessages: string[]) => {
            this.processResourcesLoadErrors(errorMessages);
          }
        );
        break;
      case WidgetActionType.mobileAction:
        const mobileAction = descriptor.mobileAction;
        this.handleMobileAction($event, mobileAction, entityId, entityName, additionalParams, entityLabel);
        break;
    }
  }

  private handleMobileAction($event: Event, mobileAction: WidgetMobileActionDescriptor,
                             entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) {
    const type = mobileAction.type;
    let argsObservable: Observable<any[]>;
    switch (type) {
      case WidgetMobileActionType.takePictureFromGallery:
      case WidgetMobileActionType.takePhoto:
      case WidgetMobileActionType.scanQrCode:
      case WidgetMobileActionType.getLocation:
      case WidgetMobileActionType.takeScreenshot:
        argsObservable = of([]);
        break;
      case WidgetMobileActionType.mapDirection:
      case WidgetMobileActionType.mapLocation:
        const getLocationFunctionString = mobileAction.getLocationFunction;
        const getLocationFunction = new Function('$event', 'widgetContext', 'entityId',
          'entityName', 'additionalParams', 'entityLabel', getLocationFunctionString);
        const locationArgs = getLocationFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
        if (locationArgs && locationArgs instanceof Observable) {
          argsObservable = locationArgs;
        } else {
          argsObservable = of(locationArgs);
        }
        argsObservable = argsObservable.pipe(map(latLng => {
          let valid = false;
          if (Array.isArray(latLng) && latLng.length === 2) {
            if (typeof latLng[0] === 'number' && typeof latLng[1] === 'number') {
              valid = true;
            }
          }
          if (valid) {
            return latLng;
          } else {
            throw new Error('Location function did not return valid array of latitude/longitude!');
          }
        }));
        break;
      case WidgetMobileActionType.makePhoneCall:
        const getPhoneNumberFunctionString = mobileAction.getPhoneNumberFunction;
        const getPhoneNumberFunction = new Function('$event', 'widgetContext', 'entityId',
          'entityName', 'additionalParams', 'entityLabel', getPhoneNumberFunctionString);
        const phoneNumberArg = getPhoneNumberFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
        if (phoneNumberArg && phoneNumberArg instanceof Observable) {
          argsObservable = phoneNumberArg.pipe(map(phoneNumber => [phoneNumber]));
        } else {
          argsObservable = of([phoneNumberArg]);
        }
        argsObservable = argsObservable.pipe(map(phoneNumberArr => {
          let valid = false;
          if (Array.isArray(phoneNumberArr) && phoneNumberArr.length === 1) {
            if (phoneNumberArr[0] !== null) {
              valid = true;
            }
          }
          if (valid) {
            return phoneNumberArr;
          } else {
            throw new Error('Phone number function did not return valid number!');
          }
        }));
        break;
    }
    argsObservable.subscribe((args) => {
      this.mobileService.handleWidgetMobileAction(type, ...args).subscribe(
        (result) => {
          if (result) {
            if (result.hasError) {
              this.handleWidgetMobileActionError(result.error, $event, mobileAction, entityId, entityName, additionalParams, entityLabel);
            } else if (result.hasResult) {
              const actionResult = result.result;
              switch (type) {
                case WidgetMobileActionType.takePictureFromGallery:
                case WidgetMobileActionType.takePhoto:
                case WidgetMobileActionType.takeScreenshot:
                  const imageUrl = actionResult.imageUrl;
                  if (mobileAction.processImageFunction && mobileAction.processImageFunction.length) {
                    try {
                      const processImageFunction = new Function('imageUrl', '$event', 'widgetContext', 'entityId',
                        'entityName', 'additionalParams', 'entityLabel', mobileAction.processImageFunction);
                      processImageFunction(imageUrl, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
                    } catch (e) {
                      console.error(e);
                    }
                  }
                  break;
                case WidgetMobileActionType.scanQrCode:
                  const code = actionResult.code;
                  const format = actionResult.format;
                  if (mobileAction.processQrCodeFunction && mobileAction.processQrCodeFunction.length) {
                    try {
                      const processQrCodeFunction = new Function('code', 'format', '$event', 'widgetContext', 'entityId',
                        'entityName', 'additionalParams', 'entityLabel', mobileAction.processQrCodeFunction);
                      processQrCodeFunction(code, format, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
                    } catch (e) {
                      console.error(e);
                    }
                  }
                  break;
                case WidgetMobileActionType.getLocation:
                  const latitude = actionResult.latitude;
                  const longitude = actionResult.longitude;
                  if (mobileAction.processLocationFunction && mobileAction.processLocationFunction.length) {
                    try {
                      const processLocationFunction = new Function('latitude', 'longitude', '$event', 'widgetContext', 'entityId',
                        'entityName', 'additionalParams', 'entityLabel', mobileAction.processLocationFunction);
                      processLocationFunction(latitude, longitude, $event, this.widgetContext,
                        entityId, entityName, additionalParams, entityLabel);
                    } catch (e) {
                      console.error(e);
                    }
                  }
                  break;
                case WidgetMobileActionType.mapDirection:
                case WidgetMobileActionType.mapLocation:
                case WidgetMobileActionType.makePhoneCall:
                  const launched = actionResult.launched;
                  if (mobileAction.processLaunchResultFunction && mobileAction.processLaunchResultFunction.length) {
                    try {
                      const processLaunchResultFunction = new Function('launched', '$event', 'widgetContext', 'entityId',
                        'entityName', 'additionalParams', 'entityLabel', mobileAction.processLaunchResultFunction);
                      processLaunchResultFunction(launched, $event, this.widgetContext,
                        entityId, entityName, additionalParams, entityLabel);
                    } catch (e) {
                      console.error(e);
                    }
                  }
                  break;
              }
            } else {
              if (mobileAction.handleEmptyResultFunction && mobileAction.handleEmptyResultFunction.length) {
                try {
                  const handleEmptyResultFunction = new Function('$event', 'widgetContext', 'entityId',
                    'entityName', 'additionalParams', 'entityLabel', mobileAction.handleEmptyResultFunction);
                  handleEmptyResultFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
                } catch (e) {
                  console.error(e);
                }
              }
            }
          }
        }
      );
    },
    (err) => {
      let errorMessage;
      if (err && typeof err === 'string') {
        errorMessage = err;
      } else if (err && err.message) {
        errorMessage = err.message;
      }
      errorMessage = `Failed to get mobile action arguments${errorMessage ? `: ${errorMessage}` : '!'}`;
      this.handleWidgetMobileActionError(errorMessage, $event, mobileAction, entityId, entityName, additionalParams, entityLabel);
    });
  }

  private handleWidgetMobileActionError(error: string, $event: Event, mobileAction: WidgetMobileActionDescriptor,
                                        entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) {
    if (mobileAction.handleErrorFunction && mobileAction.handleErrorFunction.length) {
      try {
        const handleErrorFunction = new Function('error', '$event', 'widgetContext', 'entityId',
          'entityName', 'additionalParams', 'entityLabel', mobileAction.handleErrorFunction);
        handleErrorFunction(error, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
      } catch (e) {
        console.error(e);
      }
    }
  }

  private openDashboardStateInPopover($event: Event,
                                      targetDashboardStateId: string,
                                      params?: StateParams,
                                      hideDashboardToolbar = true,
                                      preferredPlacement: PopoverPlacement = 'top',
                                      hideOnClickOutside = true,
                                      popoverWidth = '25vw',
                                      popoverHeight = '25vh',
                                      popoverStyle: { [klass: string]: any } = {}) {
    const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element;
    if (this.popoverService.hasPopover(trigger)) {
      this.popoverService.hidePopover(trigger);
    } else {
      const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard());
      const stateObject: StateObject = {};
      stateObject.params = params;
      if (targetDashboardStateId) {
        stateObject.id = targetDashboardStateId;
      }
      const injector = Injector.create({
        parent: this.widgetContentContainer.injector, providers: [
          {
            provide: 'embeddedValue',
            useValue: true
          }
        ]
      });
      const component = this.popoverService.displayPopover(trigger, this.renderer,
        this.widgetContentContainer, this.dashboardPageComponent, preferredPlacement, hideOnClickOutside,
        injector,
        {
          embedded: true,
          syncStateWithQueryParam: false,
          hideToolbar: hideDashboardToolbar,
          currentState: objToBase64([stateObject]),
          dashboard,
          parentDashboard: this.widgetContext.parentDashboard ?
            this.widgetContext.parentDashboard : this.widgetContext.dashboard
        },
        {width: popoverWidth, height: popoverHeight},
        popoverStyle,
        {}
      );
      this.widgetContext.registerPopoverComponent(component);
    }
  }

  private openDashboardStateInSeparateDialog(targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
                                             hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number) {
    const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard());
    const stateObject: StateObject = {};
    stateObject.params = params;
    if (targetDashboardStateId) {
      stateObject.id = targetDashboardStateId;
    }
    let title = dialogTitle;
    if (!title) {
      if (targetDashboardStateId && dashboard.configuration.states) {
        const dashboardState = dashboard.configuration.states[targetDashboardStateId];
        if (dashboardState) {
          title = dashboardState.name;
        }
      }
    }
    if (!title) {
      title = dashboard.title;
    }
    title = this.utils.customTranslation(title, title);
    const paramsEntityName = params && params.entityName ? params.entityName : '';
    const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : '';
    title = insertVariable(title, 'entityName', paramsEntityName);
    title = insertVariable(title, 'entityLabel', paramsEntityLabel);
    for (const prop of Object.keys(params)) {
      if (params[prop] && params[prop].entityName) {
        title = insertVariable(title, prop + ':entityName', params[prop].entityName);
      }
      if (params[prop] && params[prop].entityLabel) {
        title = insertVariable(title, prop + ':entityLabel', params[prop].entityLabel);
      }
    }
    this.dialog.open(this.embedDashboardDialogComponent, {
      disableClose: true,
      panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
      viewContainerRef: this.widgetContentContainer,
      data: {
        dashboard,
        state: objToBase64([ stateObject ]),
        title,
        hideToolbar: hideDashboardToolbar,
        width: dialogWidth,
        height: dialogHeight
      }
    });
    this.cd.markForCheck();
  }

  private elementClick($event: Event) {
    const elementClicked = ($event.target || $event.srcElement) as Element;
    const descriptors = this.getActionDescriptors('elementClick');
    if (descriptors.length) {
      const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(',');
      const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]);
      if (targetElement.length && targetElement[0].id) {
        $event.stopPropagation();
        const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id);
        const entityInfo = this.getActiveEntityInfo();
        const entityId = entityInfo ? entityInfo.entityId : null;
        const entityName = entityInfo ? entityInfo.entityName : null;
        const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;
        this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);
      }
    }
  }

  private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>): Observable<any> {
    if (isDefined(customCss) && customCss.length > 0) {
      this.cssParser.cssPreviewNamespace = actionNamespace;
      this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace');
    }
    const resourceTasks: Observable<string>[] = [];
    if (isDefined(customResources) && customResources.length > 0) {
      customResources.forEach((resource) => {
        resourceTasks.push(
          this.resources.loadResource(resource.url).pipe(
            catchError(e => of(`Failed to load custom action resource: '${resource.url}'`))
          )
        );
      });
      return forkJoin(resourceTasks).pipe(
        switchMap(msgs => {
            let errors: string[];
            if (msgs && msgs.length) {
              errors = msgs.filter(msg => msg && msg.length > 0);
            }
            if (errors && errors.length) {
              return throwError(errors);
            } else {
              return of(null);
            }
          }
      ));
    } else {
      return of(null);
    }
  }

  private processResourcesLoadErrors(errorMessages: string[]) {
    let messageToShow = '';
    errorMessages.forEach(error => {
      messageToShow += `<div>${error}</div>`;
    });
    this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'}));
  }

  private getActiveEntityInfo(): SubscriptionEntityInfo {
    let entityInfo = this.widgetContext.activeEntityInfo;
    if (!entityInfo) {
      for (const id of Object.keys(this.widgetContext.subscriptions)) {
        const subscription = this.widgetContext.subscriptions[id];
        entityInfo = subscription.getFirstEntityInfo();
        if (entityInfo) {
          break;
        }
      }
    }
    return entityInfo;
  }

  private checkSize(): boolean {
    const width = this.widgetContext.$containerParent.width();
    const height = this.widgetContext.$containerParent.height();
    let sizeChanged = false;

    if (!this.widgetContext.width || this.widgetContext.width !== width ||
      !this.widgetContext.height || this.widgetContext.height !== height) {
      if (width > 0 && height > 0) {
        if (this.widgetContext.$container) {
          this.widgetContext.$container.css('height', height + 'px');
          this.widgetContext.$container.css('width', width + 'px');
        }
        this.widgetContext.width = width;
        this.widgetContext.height = height;
        sizeChanged = true;
        this.widgetSizeDetected = true;
      }
    }
    return sizeChanged;
  }

}
