import Tab from './web/tab';
import handleContentMessage from './web/content';
import { rules, createAutoCMP } from './index';
import { Browser, MessageSender, AutoCMP, TabActor } from './types';
import { ConsentOMaticCMP, ConsentOMaticConfig } from './consentomatic/index';
import { AutoConsentCMPRule } from './rules';

export * from './index';
export {
  Tab,
  handleContentMessage,
}

class TabConsent {
  checked: Promise<AutoCMP>
  rule: AutoCMP
  optOutStatus: boolean | Error = null

  constructor(public tab: TabActor, ruleCheckPromise: Promise<AutoCMP>) {
    this.checked = ruleCheckPromise;
    ruleCheckPromise.then(rule => this.rule = rule);
  }

  getCMPName() {
    if (this.rule) {
      return this.rule.name;
    }
    return null;
  }

  async isPopupOpen(retries = 1, interval = 1000) {
    const isOpen = await this.rule.detectPopup(this.tab);
    if (!isOpen && retries > 0) {
      return new Promise((resolve) => setTimeout(() => resolve(this.isPopupOpen(retries - 1, interval)), interval));
    }
    return isOpen;
  }

  async doOptOut() {
    try {
      this.optOutStatus = await this.rule.optOut(this.tab);
      return this.optOutStatus;
    } catch (e) {
      this.optOutStatus = e;
      throw e;
    }
  }

  async doOptIn() {
    return this.rule.optIn(this.tab);
  }

  hasTest() {
    return !!this.rule.hasSelfTest
  }

  async testOptOutWorked() {
    return this.rule.test(this.tab);
  }

  async applyCosmetics(selectors: string[]) {
    const hidden = await this.tab.hideElements(selectors);
    return hidden;
  }
}

export default class AutoConsent {
  consentFrames: Map<number, any> = new Map()
  tabCmps: Map<number, TabConsent> = new Map()
  rules: AutoCMP[]

  constructor(protected browser: Browser, protected sendContentMessage: MessageSender) {
    this.sendContentMessage = sendContentMessage;
    this.rules = [...rules];
  }

  addCMP(config: AutoConsentCMPRule) {
    this.rules.push(createAutoCMP(config));
  }

  disableCMPs(cmpNames: String[]) {
    this.rules = this.rules.filter((cmp) => !cmpNames.includes(cmp.name))
  }

  addConsentomaticCMP(name: string, config: ConsentOMaticConfig) {
    this.rules.push(new ConsentOMaticCMP(`com_${name}`, config));
  }

  createTab(tabId: number) {
    return new Tab(tabId,
      this.consentFrames.get(tabId),
      this.sendContentMessage,
      this.browser);
  }

  async checkTab(tabId: number) {
    const tab = this.createTab(tabId);
    const consent = new TabConsent(tab, this.detectDialog(tab, 20));
    this.tabCmps.set(tabId, consent);
    // check tabs
    consent.checked.then((rule) => {
      if (this.consentFrames.has(tabId) && rule) {
        const frame = this.consentFrames.get(tabId);
        if (frame.type === rule.name) {
          consent.tab.frame = frame;
        }
      }
    });

    return this.tabCmps.get(tabId);
  }

  removeTab(tabId: number) {
    this.tabCmps.delete(tabId);
    this.consentFrames.delete(tabId);
  }

  onFrame({ tabId, url, frameId }: { tabId: number, url: string, frameId: number }) {
    // ignore main frames
    if (frameId === 0) {
      return;
    }
    try {
      const frame = {
        id: frameId,
        url: url,
      };
      const tab = this.createTab(tabId);
      const frameMatch = this.rules.findIndex(r => r.detectFrame(tab, frame));
      if (frameMatch > -1) {
        this.consentFrames.set(tabId, {
          type: this.rules[frameMatch].name,
          url,
          id: frameId,
        });
        if (this.tabCmps.has(tabId)) {
          this.tabCmps.get(tabId).tab.frame = this.consentFrames.get(tabId);
        }
      }
    } catch (e) {
      console.error(e);
    }
  }

  async detectDialog(tab: TabActor, retries: number): Promise<AutoCMP> {
    const found: number = await new Promise(async (resolve) => {
      let earlyReturn = false;
      await Promise.all(this.rules.map(async (r, index) => {
        try {
          if (await r.detectCmp(tab)) {
            earlyReturn = true;
            resolve(index)
          }
        } catch (e) {
          console.warn('detectCMP error', r.name, e)
        }
      }));
      if (!earlyReturn) {
        resolve(-1)
      }
    })
    if (found === -1 && retries > 0) {
      return new Promise((resolve) => {
        setTimeout(async () => {
          const result = this.detectDialog(tab, retries - 1);
          resolve(result);
        }, 500);
      });
    }
    return found > -1 ? this.rules[found] : null;
  }
}
