import { Event, EventTeaser, ExceptionObject } from "../core/typings";

import { debug } from "../logging";

import CfSender from "./CfSender";
import { ContentBlock } from "../modules/Navigation/typings";

import ImpressionsDetector from "./impressions";
import { ImpressionEventType } from "./impressions/typings";
import CfExceptionHandler from "./CfExceptionHandler";
import { getLocalStorageUserId } from "../drivers/CfSystem";
import { CatalogItemModel } from "../modules/typings";
import CfNetwork from "../drivers/network/CfNetwork";
import { isOnline } from "../drivers/network/helper";

export default class CfCore {
  private static instance: CfCore;
  private deviceId: string;
  private sender: CfSender;
  private isUsingAsInternalWrapper: boolean;
  private impressionDetectorClass;
  private impressionsDetectors: {
    [key: string]: ImpressionsDetector;
  } = {};
  private defaultBlock;
  private debug;
  private currentBlock;

  private user_id = "";

  private constructor(
    sender: CfSender,
    impressionDetector,
    debugging: boolean,
    defaultBlock: ContentBlock,
    deviceId: string,
    isUsingAsInternalWrapper: boolean
  ) {
    this.sender = sender;
    this.impressionDetectorClass = impressionDetector;

    this.deviceId = deviceId;
    this.defaultBlock = defaultBlock;
    this.debug = debug(debugging);
    this.isUsingAsInternalWrapper = isUsingAsInternalWrapper;
  }

  public static createInstance(
    sender: CfSender,
    impressionDetectorClass,
    debugging: boolean,
    defaultBlock: ContentBlock,
    deviceId: string,
    isUsingAsInternalWrapper: boolean
  ) {
    if (CfCore.instance) {
      return CfCore.instance;
    }

    CfCore.instance = new CfCore(
      sender,
      impressionDetectorClass,
      debugging,
      defaultBlock,
      deviceId,
      isUsingAsInternalWrapper
    );
    return CfCore.instance;
  }

  public static getInstance(): CfCore {
    if (!CfCore.instance) {
      new CfExceptionHandler().throwAndLogError(
        "CfLog",
        "CfLog instance not created"
      );
    }

    return CfCore.instance;
  }

  public login(userId: string) {
    this.user_id = userId;
  }

  private resolveModule(module: ContentBlock): ContentBlock {
    // logPageEvents are triggered automatically by the system. We do not
    // know whether the web application calls setCurrentBlock before or after that:
    const delta = 50;

    if (module === ContentBlock.Core) {
      // in this case the event could be triggered by several modules
      // so the SDK must decide which one must be included in the event
      // sent to server (i.e.: page or media events)
      if (this.currentBlock) {
        return this.currentBlock;
      } else {
        return this.defaultBlock;
      }
    } else {
      // specific module (i.e.: triggered by the ELearning module)
      return module;
    }
  }

  private resolveUserId(
    overWrittedUserId: string,
    user_id: string,
    device_id: string
  ): string {
    if (overWrittedUserId && overWrittedUserId.length !== 0) {
      return overWrittedUserId;
    }

    return user_id || getLocalStorageUserId() || device_id;
  }

  public async trackEvent(
    eventTeaser: EventTeaser,
    overWrittedUserId: string,
    sendNow: boolean,
    eventTime?: string
  ): Promise<void> {
    const event: Event = {
      ...eventTeaser,
      time: eventTime || new Date().toISOString(),
    };

    // Ensure ctx exists and add ol property
    event.ctx = event.ctx || {};
    event.ctx.__ol = isOnline();

    this.sender.add(
      this.resolveUserId(overWrittedUserId, this.user_id, this.deviceId),
      this.deviceId,
      event,
      sendNow,
      this.isUsingAsInternalWrapper
    );
  }

  public async trackCatalog(catalogItem: CatalogItemModel): Promise<void> {
    this.sender.addCatalogItem(catalogItem);
  }

  public startTrackingImpressions(
    impressionHandler,
    containerClassname,
    itemClassname
  ) {
    this.impressionsDetectors[containerClassname] =
      new this.impressionDetectorClass({ intersectionThreshold: 0.5 });

    this.impressionsDetectors[containerClassname].on(
      ImpressionEventType.Impression,
      impressionHandler
    );

    this.impressionsDetectors[containerClassname].start(
      containerClassname,
      itemClassname,
      {}
    );
  }

  public stopTrackingImpressions(containerClassname) {
    if (!containerClassname) {
      Object.values(this.impressionsDetectors).forEach((detector) =>
        detector.stop()
      );
      return;
    }

    if (!this.impressionsDetectors[containerClassname]) {
      return;
    }

    this.impressionsDetectors[containerClassname].stop();
  }

  public restartTrackingImpressions(containerClassname) {
    if (!containerClassname || !this.impressionsDetectors[containerClassname]) {
      console.error(
        "Impossible to restart impression to unknown containerClassname"
      );
      throw new Error("unknown-container");
    }

    this.impressionsDetectors[containerClassname].restart({});
  }

  public setCurrentBlock(block: ContentBlock) {
    // the reason of this delay is that the page event is triggered
    // 10 miliseconds after it really happens to wait for the title
    setTimeout(() => {
      this.currentBlock = block;
    }, 15);
  }
}
