async (name: string) => {
      const expression = helper.pageBindingInitString('internal', name);
      try {
        // TODO: In theory, it would be enough to call this just once
        await context._client.send('Runtime.addBinding', {
          name,
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-ignore The protocol definition is not up to date.
          executionContextName: context._contextName,
        });
        await context.evaluate(expression);
      } catch (error) {
        // We could have tried to evaluate in a context which was already
        // destroyed. This happens, for example, if the page is navigated while
        // we are trying to add the binding
        const ctxDestroyed = error.message.includes(
          'Execution context was destroyed'
        );
        const ctxNotFound = error.message.includes(
          'Cannot find context with specified id'
        );
        if (ctxDestroyed || ctxNotFound) {
          return;
        } else {
          debugError(error);
          return;
        }
      }
      this._ctxBindings.add(
        DOMWorld.bindingIdentifier(name, context._contextId)
      );
    }


() => document.title


(resolve, reject) => {
      this._resolve = resolve;
      this._reject = reject;
    }


() => this.terminate(timeoutError)


(s) => !s


() => (timedOut = true)


(x) => (fulfill = x)


async () => {
      if (timedOut) {
        observer.disconnect();
        fulfill();
      }
      const success = await predicate(...args);
      if (success) {
        observer.disconnect();
        fulfill(success);
      }
    }


/**
 * @internal
 */
export class WaitTask {
  _domWorld: DOMWorld;
  _polling: string | number;
  _timeout: number;
  _predicateBody: string;
  _args: SerializableOrJSHandle[];
  _binding: PageBinding;
  _runCount = 0;
  promise: Promise<JSHandle>;
  _resolve: (x: JSHandle) => void;
  _reject: (x: Error) => void;
  _timeoutTimer?: NodeJS.Timeout;
  _terminated = false;

  constructor(options: WaitTaskOptions) {
    if (helper.isString(options.polling))
      assert(
        options.polling === 'raf' || options.polling === 'mutation',
        'Unknown polling option: ' + options.polling
      );
    else if (helper.isNumber(options.polling))
      assert(
        options.polling > 0,
        'Cannot poll with non-positive interval: ' + options.polling
      );
    else throw new Error('Unknown polling options: ' + options.polling);

    function getPredicateBody(predicateBody: Function | string) {
      if (helper.isString(predicateBody)) return `return (${predicateBody});`;
      return `return (${predicateBody})(...args);`;
    }

    this._domWorld = options.domWorld;
    this._polling = options.polling;
    this._timeout = options.timeout;
    this._predicateBody = getPredicateBody(options.predicateBody);
    this._args = options.args;
    this._binding = options.binding;
    this._runCount = 0;
    this._domWorld._waitTasks.add(this);
    if (this._binding) {
      this._domWorld._boundFunctions.set(
        this._binding.name,
        this._binding.pptrFunction
      );
    }
    this.promise = new Promise<JSHandle>((resolve, reject) => {
      this._resolve = resolve;
      this._reject = reject;
    });
    // Since page navigation requires us to re-install the pageScript, we should track
    // timeout on our end.
    if (options.timeout) {
      const timeoutError = new TimeoutError(
        `waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded`
      );
      this._timeoutTimer = setTimeout(
        () => this.terminate(timeoutError),
        options.timeout
      );
    }
    this.rerun();
  }

  terminate(error: Error): void {
    this._terminated = true;
    this._reject(error);
    this._cleanup();
  }

  async rerun(): Promise<void> {
    const runCount = ++this._runCount;
    let success: JSHandle = null;
    let error: Error = null;
    const context = await this._domWorld.executionContext();
    if (this._terminated || runCount !== this._runCount) return;
    if (this._binding) {
      await this._domWorld.addBindingToContext(context, this._binding.name);
    }
    if (this._terminated || runCount !== this._runCount) return;
    try {
      success = await context.evaluateHandle(
        waitForPredicatePageFunction,
        this._predicateBody,
        this._polling,
        this._timeout,
        ...this._args
      );
    } catch (error_) {
      error = error_;
    }

    if (this._terminated || runCount !== this._runCount) {
      if (success) await success.dispose();
      return;
    }

    // Ignore timeouts in pageScript - we track timeouts ourselves.
    // If the frame's execution context has already changed, `frame.evaluate` will
    // throw an error - ignore this predicate run altogether.
    if (
      !error &&
      (await this._domWorld.evaluate((s) => !s, success).catch(() => true))
    ) {
      await success.dispose();
      return;
    }
    if (error) {
      if (error.message.includes('TypeError: binding is not a function')) {
        return this.rerun();
      }
      // When frame is detached the task should have been terminated by the DOMWorld.
      // This can fail if we were adding this task while the frame was detached,
      // so we terminate here instead.
      if (
        error.message.includes(
          'Execution context is not available in detached frame'
        )
      ) {
        this.terminate(
          new Error('waitForFunction failed: frame got detached.')
        );
        return;
      }

      // When the page is navigated, the promise is rejected.
      // We will try again in the new execution context.
      if (error.message.includes('Execution context was destroyed')) return;

      // We could have tried to evaluate in a context which was already
      // destroyed.
      if (error.message.includes('Cannot find context with specified id'))
        return;

      this._reject(error);
    } else {
      this._resolve(success);
    }
    this._cleanup();
  }

  _cleanup(): void {
    clearTimeout(this._timeoutTimer);
    this._domWorld._waitTasks.delete(this);
  }
}


/**
 * @public
 */
export interface WaitForSelectorOptions {
  visible?: boolean;
  hidden?: boolean;
  timeout?: number;
}


/**
 * @internal
 */
export interface PageBinding {
  name: string;
  pptrFunction: Function;
}


/**
 * @internal
 */
export interface WaitTaskOptions {
  domWorld: DOMWorld;
  predicateBody: Function | string;
  title: string;
  polling: string | number;
  timeout: number;
  binding?: PageBinding;
  args: SerializableOrJSHandle[];
}


frame(): Frame {
    return this._frame;
  }


async _setContext(context?: ExecutionContext): Promise<void> {
    if (context) {
      this._ctxBindings.clear();
      this._contextResolveCallback.call(null, context);
      this._contextResolveCallback = null;
      for (const waitTask of this._waitTasks) waitTask.rerun();
    } else {
      this._documentPromise = null;
      this._contextPromise = new Promise((fulfill) => {
        this._contextResolveCallback = fulfill;
      });
    }
  }


_hasContext(): boolean {
    return !this._contextResolveCallback;
  }


_detach(): void {
    this._detached = true;
    for (const waitTask of this._waitTasks)
      waitTask.terminate(
        new Error('waitForFunction failed: frame got detached.')
      );
  }


executionContext(): Promise<ExecutionContext> {
    if (this._detached)
      throw new Error(
        `Execution context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`
      );
    return this._contextPromise;
  }


async evaluateHandle<HandlerType extends JSHandle = JSHandle>(
    pageFunction: EvaluateHandleFn,
    ...args: SerializableOrJSHandle[]
  ): Promise<HandlerType> {
    const context = await this.executionContext();
    return context.evaluateHandle(pageFunction, ...args);
  }


async evaluate<T extends EvaluateFn>(
    pageFunction: T,
    ...args: SerializableOrJSHandle[]
  ): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
    const context = await this.executionContext();
    return context.evaluate<UnwrapPromiseLike<EvaluateFnReturnType<T>>>(
      pageFunction,
      ...args
    );
  }


async $<T extends Element = Element>(
    selector: string
  ): Promise<ElementHandle<T> | null> {
    const document = await this._document();
    const value = await document.$<T>(selector);
    return value;
  }


async _document(): Promise<ElementHandle> {
    if (this._documentPromise) return this._documentPromise;
    this._documentPromise = this.executionContext().then(async (context) => {
      const document = await context.evaluateHandle('document');
      return document.asElement();
    });
    return this._documentPromise;
  }


async $x(expression: string): Promise<ElementHandle[]> {
    const document = await this._document();
    const value = await document.$x(expression);
    return value;
  }


async $eval<ReturnType>(
    selector: string,
    pageFunction: (
      element: Element,
      ...args: unknown[]
    ) => ReturnType | Promise<ReturnType>,
    ...args: SerializableOrJSHandle[]
  ): Promise<WrapElementHandle<ReturnType>> {
    const document = await this._document();
    return document.$eval<ReturnType>(selector, pageFunction, ...args);
  }


async $$eval<ReturnType>(
    selector: string,
    pageFunction: (
      elements: Element[],
      ...args: unknown[]
    ) => ReturnType | Promise<ReturnType>,
    ...args: SerializableOrJSHandle[]
  ): Promise<WrapElementHandle<ReturnType>> {
    const document = await this._document();
    const value = await document.$$eval<ReturnType>(
      selector,
      pageFunction,
      ...args
    );
    return value;
  }


async $$<T extends Element = Element>(
    selector: string
  ): Promise<Array<ElementHandle<T>>> {
    const document = await this._document();
    const value = await document.$$<T>(selector);
    return value;
  }


async content(): Promise<string> {
    return await this.evaluate(() => {
      let retVal = '';
      if (document.doctype)
        retVal = new XMLSerializer().serializeToString(document.doctype);
      if (document.documentElement)
        retVal += document.documentElement.outerHTML;
      return retVal;
    });
  }


async setContent(
    html: string,
    options: {
      timeout?: number;
      waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
    } = {}
  ): Promise<void> {
    const {
      waitUntil = ['load'],
      timeout = this._timeoutSettings.navigationTimeout(),
    } = options;
    // We rely upon the fact that document.open() will reset frame lifecycle with "init"
    // lifecycle event. @see https://crrev.com/608658
    await this.evaluate<(x: string) => void>((html) => {
      document.open();
      document.write(html);
      document.close();
    }, html);
    const watcher = new LifecycleWatcher(
      this._frameManager,
      this._frame,
      waitUntil,
      timeout
    );
    const error = await Promise.race([
      watcher.timeoutOrTerminationPromise(),
      watcher.lifecyclePromise(),
    ]);
    watcher.dispose();
    if (error) throw error;
  }


/**
   * Adds a script tag into the current context.
   *
   * @remarks
   *
   * You can pass a URL, filepath or string of contents. Note that when running Puppeteer
   * in a browser environment you cannot pass a filepath and should use either
   * `url` or `content`.
   */
async addScriptTag(options: {
    url?: string;
    path?: string;
    content?: string;
    type?: string;
  }): Promise<ElementHandle> {
    const { url = null, path = null, content = null, type = '' } = options;
    if (url !== null) {
      try {
        const context = await this.executionContext();
        return (
          await context.evaluateHandle(addScriptUrl, url, type)
        ).asElement();
      } catch (error) {
        throw new Error(`Loading script from ${url} failed`);
      }
    }

    if (path !== null) {
      if (!isNode) {
        throw new Error(
          'Cannot pass a filepath to addScriptTag in the browser environment.'
        );
      }
      const fs = await helper.importFSModule();
      let contents = await fs.promises.readFile(path, 'utf8');
      contents += '//# sourceURL=' + path.replace(/\n/g, '');
      const context = await this.executionContext();
      return (
        await context.evaluateHandle(addScriptContent, contents, type)
      ).asElement();
    }

    if (content !== null) {
      const context = await this.executionContext();
      return (
        await context.evaluateHandle(addScriptContent, content, type)
      ).asElement();
    }

    throw new Error(
      'Provide an object with a `url`, `path` or `content` property'
    );

    async function addScriptUrl(
      url: string,
      type: string
    ): Promise<HTMLElement> {
      const script = document.createElement('script');
      script.src = url;
      if (type) script.type = type;
      const promise = new Promise((res, rej) => {
        script.onload = res;
        script.onerror = rej;
      });
      document.head.appendChild(script);
      await promise;
      return script;
    }

    function addScriptContent(
      content: string,
      type = 'text/javascript'
    ): HTMLElement {
      const script = document.createElement('script');
      script.type = type;
      script.text = content;
      let error = null;
      script.onerror = (e) => (error = e);
      document.head.appendChild(script);
      if (error) throw error;
      return script;
    }
  }


/**
   * Adds a style tag into the current context.
   *
   * @remarks
   *
   * You can pass a URL, filepath or string of contents. Note that when running Puppeteer
   * in a browser environment you cannot pass a filepath and should use either
   * `url` or `content`.
   *
   */
async addStyleTag(options: {
    url?: string;
    path?: string;
    content?: string;
  }): Promise<ElementHandle> {
    const { url = null, path = null, content = null } = options;
    if (url !== null) {
      try {
        const context = await this.executionContext();
        return (await context.evaluateHandle(addStyleUrl, url)).asElement();
      } catch (error) {
        throw new Error(`Loading style from ${url} failed`);
      }
    }

    if (path !== null) {
      if (!isNode) {
        throw new Error(
          'Cannot pass a filepath to addStyleTag in the browser environment.'
        );
      }
      const fs = await helper.importFSModule();
      let contents = await fs.promises.readFile(path, 'utf8');
      contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
      const context = await this.executionContext();
      return (
        await context.evaluateHandle(addStyleContent, contents)
      ).asElement();
    }

    if (content !== null) {
      const context = await this.executionContext();
      return (
        await context.evaluateHandle(addStyleContent, content)
      ).asElement();
    }

    throw new Error(
      'Provide an object with a `url`, `path` or `content` property'
    );

    async function addStyleUrl(url: string): Promise<HTMLElement> {
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = url;
      const promise = new Promise((res, rej) => {
        link.onload = res;
        link.onerror = rej;
      });
      document.head.appendChild(link);
      await promise;
      return link;
    }

    async function addStyleContent(content: string): Promise<HTMLElement> {
      const style = document.createElement('style');
      style.type = 'text/css';
      style.appendChild(document.createTextNode(content));
      const promise = new Promise((res, rej) => {
        style.onload = res;
        style.onerror = rej;
      });
      document.head.appendChild(style);
      await promise;
      return style;
    }
  }


async click(
    selector: string,
    options: { delay?: number; button?: MouseButton; clickCount?: number }
  ): Promise<void> {
    const handle = await this.$(selector);
    assert(handle, 'No node found for selector: ' + selector);
    await handle.click(options);
    await handle.dispose();
  }


async focus(selector: string): Promise<void> {
    const handle = await this.$(selector);
    assert(handle, 'No node found for selector: ' + selector);
    await handle.focus();
    await handle.dispose();
  }


async hover(selector: string): Promise<void> {
    const handle = await this.$(selector);
    assert(handle, 'No node found for selector: ' + selector);
    await handle.hover();
    await handle.dispose();
  }


async select(selector: string, ...values: string[]): Promise<string[]> {
    const handle = await this.$(selector);
    assert(handle, 'No node found for selector: ' + selector);
    const result = await handle.select(...values);
    await handle.dispose();
    return result;
  }


async tap(selector: string): Promise<void> {
    const handle = await this.$(selector);
    await handle.tap();
    await handle.dispose();
  }


async type(
    selector: string,
    text: string,
    options?: { delay: number }
  ): Promise<void> {
    const handle = await this.$(selector);
    assert(handle, 'No node found for selector: ' + selector);
    await handle.type(text, options);
    await handle.dispose();
  }


async waitForSelector(
    selector: string,
    options: WaitForSelectorOptions
  ): Promise<ElementHandle | null> {
    const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
      selector
    );
    return queryHandler.waitFor(this, updatedSelector, options);
  }


/**
   * @internal
   */
async addBindingToContext(
    context: ExecutionContext,
    name: string
  ): Promise<void> {
    // Previous operation added the binding so we are done.
    if (
      this._ctxBindings.has(
        DOMWorld.bindingIdentifier(name, context._contextId)
      )
    ) {
      return;
    }
    // Wait for other operation to finish
    if (this._settingUpBinding) {
      await this._settingUpBinding;
      return this.addBindingToContext(context, name);
    }

    const bind = async (name: string) => {
      const expression = helper.pageBindingInitString('internal', name);
      try {
        // TODO: In theory, it would be enough to call this just once
        await context._client.send('Runtime.addBinding', {
          name,
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-ignore The protocol definition is not up to date.
          executionContextName: context._contextName,
        });
        await context.evaluate(expression);
      } catch (error) {
        // We could have tried to evaluate in a context which was already
        // destroyed. This happens, for example, if the page is navigated while
        // we are trying to add the binding
        const ctxDestroyed = error.message.includes(
          'Execution context was destroyed'
        );
        const ctxNotFound = error.message.includes(
          'Cannot find context with specified id'
        );
        if (ctxDestroyed || ctxNotFound) {
          return;
        } else {
          debugError(error);
          return;
        }
      }
      this._ctxBindings.add(
        DOMWorld.bindingIdentifier(name, context._contextId)
      );
    };

    this._settingUpBinding = bind(name);
    await this._settingUpBinding;
    this._settingUpBinding = null;
  }


private async _onBindingCalled(
    event: Protocol.Runtime.BindingCalledEvent
  ): Promise<void> {
    let payload: { type: string; name: string; seq: number; args: unknown[] };
    if (!this._hasContext()) return;
    const context = await this.executionContext();
    try {
      payload = JSON.parse(event.payload);
    } catch {
      // The binding was either called by something in the page or it was
      // called before our wrapper was initialized.
      return;
    }
    const { type, name, seq, args } = payload;
    if (
      type !== 'internal' ||
      !this._ctxBindings.has(
        DOMWorld.bindingIdentifier(name, context._contextId)
      )
    )
      return;
    if (context._contextId !== event.executionContextId) return;
    try {
      const result = await this._boundFunctions.get(name)(...args);
      await context.evaluate(deliverResult, name, seq, result);
    } catch (error) {
      // The WaitTask may already have been resolved by timing out, or the
      // exection context may have been destroyed.
      // In both caes, the promises above are rejected with a protocol error.
      // We can safely ignores these, as the WaitTask is re-installed in
      // the next execution context if needed.
      if (error.message.includes('Protocol error')) return;
      debugError(error);
    }
    function deliverResult(name: string, seq: number, result: unknown): void {
      globalThis[name].callbacks.get(seq).resolve(result);
      globalThis[name].callbacks.delete(seq);
    }
  }


/**
   * @internal
   */
async waitForSelectorInPage(
    queryOne: Function,
    selector: string,
    options: WaitForSelectorOptions,
    binding?: PageBinding
  ): Promise<ElementHandle | null> {
    const {
      visible: waitForVisible = false,
      hidden: waitForHidden = false,
      timeout = this._timeoutSettings.timeout(),
    } = options;
    const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
    const title = `selector \`${selector}\`${
      waitForHidden ? ' to be hidden' : ''
    }`;
    async function predicate(
      selector: string,
      waitForVisible: boolean,
      waitForHidden: boolean
    ): Promise<Node | null | boolean> {
      const node = predicateQueryHandler
        ? ((await predicateQueryHandler(document, selector)) as Element)
        : document.querySelector(selector);
      return checkWaitForOptions(node, waitForVisible, waitForHidden);
    }
    const waitTaskOptions: WaitTaskOptions = {
      domWorld: this,
      predicateBody: helper.makePredicateString(predicate, queryOne),
      title,
      polling,
      timeout,
      args: [selector, waitForVisible, waitForHidden],
      binding,
    };
    const waitTask = new WaitTask(waitTaskOptions);
    const jsHandle = await waitTask.promise;
    const elementHandle = jsHandle.asElement();
    if (!elementHandle) {
      await jsHandle.dispose();
      return null;
    }
    return elementHandle;
  }


async waitForXPath(
    xpath: string,
    options: WaitForSelectorOptions
  ): Promise<ElementHandle | null> {
    const {
      visible: waitForVisible = false,
      hidden: waitForHidden = false,
      timeout = this._timeoutSettings.timeout(),
    } = options;
    const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
    const title = `XPath \`${xpath}\`${waitForHidden ? ' to be hidden' : ''}`;
    function predicate(
      xpath: string,
      waitForVisible: boolean,
      waitForHidden: boolean
    ): Node | null | boolean {
      const node = document.evaluate(
        xpath,
        document,
        null,
        XPathResult.FIRST_ORDERED_NODE_TYPE,
        null
      ).singleNodeValue;
      return checkWaitForOptions(node, waitForVisible, waitForHidden);
    }
    const waitTaskOptions: WaitTaskOptions = {
      domWorld: this,
      predicateBody: helper.makePredicateString(predicate),
      title,
      polling,
      timeout,
      args: [xpath, waitForVisible, waitForHidden],
    };
    const waitTask = new WaitTask(waitTaskOptions);
    const jsHandle = await waitTask.promise;
    const elementHandle = jsHandle.asElement();
    if (!elementHandle) {
      await jsHandle.dispose();
      return null;
    }
    return elementHandle;
  }


waitForFunction(
    pageFunction: Function | string,
    options: { polling?: string | number; timeout?: number } = {},
    ...args: SerializableOrJSHandle[]
  ): Promise<JSHandle> {
    const {
      polling = 'raf',
      timeout = this._timeoutSettings.timeout(),
    } = options;
    const waitTaskOptions: WaitTaskOptions = {
      domWorld: this,
      predicateBody: pageFunction,
      title: 'function',
      polling,
      timeout,
      args,
    };
    const waitTask = new WaitTask(waitTaskOptions);
    return waitTask.promise;
  }


async title(): Promise<string> {
    return this.evaluate(() => document.title);
  }


terminate(error: Error): void {
    this._terminated = true;
    this._reject(error);
    this._cleanup();
  }


async rerun(): Promise<void> {
    const runCount = ++this._runCount;
    let success: JSHandle = null;
    let error: Error = null;
    const context = await this._domWorld.executionContext();
    if (this._terminated || runCount !== this._runCount) return;
    if (this._binding) {
      await this._domWorld.addBindingToContext(context, this._binding.name);
    }
    if (this._terminated || runCount !== this._runCount) return;
    try {
      success = await context.evaluateHandle(
        waitForPredicatePageFunction,
        this._predicateBody,
        this._polling,
        this._timeout,
        ...this._args
      );
    } catch (error_) {
      error = error_;
    }

    if (this._terminated || runCount !== this._runCount) {
      if (success) await success.dispose();
      return;
    }

    // Ignore timeouts in pageScript - we track timeouts ourselves.
    // If the frame's execution context has already changed, `frame.evaluate` will
    // throw an error - ignore this predicate run altogether.
    if (
      !error &&
      (await this._domWorld.evaluate((s) => !s, success).catch(() => true))
    ) {
      await success.dispose();
      return;
    }
    if (error) {
      if (error.message.includes('TypeError: binding is not a function')) {
        return this.rerun();
      }
      // When frame is detached the task should have been terminated by the DOMWorld.
      // This can fail if we were adding this task while the frame was detached,
      // so we terminate here instead.
      if (
        error.message.includes(
          'Execution context is not available in detached frame'
        )
      ) {
        this.terminate(
          new Error('waitForFunction failed: frame got detached.')
        );
        return;
      }

      // When the page is navigated, the promise is rejected.
      // We will try again in the new execution context.
      if (error.message.includes('Execution context was destroyed')) return;

      // We could have tried to evaluate in a context which was already
      // destroyed.
      if (error.message.includes('Cannot find context with specified id'))
        return;

      this._reject(error);
    } else {
      this._resolve(success);
    }
    this._cleanup();
  }


_cleanup(): void {
    clearTimeout(this._timeoutTimer);
    this._domWorld._waitTasks.delete(this);
  }


function getSelectedHighlightTop() {
    const nextSelectedElement: HTMLDivElement = fixture.debugElement.query(
      By.css('.highlight-selected')
    ).nativeElement;

    return nextSelectedElement.offsetTop;
  }


(currentLogList: LogEntry[]): LogEntry[] => {
  const logList = [...currentLogList];
  for (let i = 1; i <= 15; i++) {
    logList.push({
      timestamp: '2019-08-19T12:07:00.1222053Z',
      message: 'Just for test',
      container: 'test-container',
    });
  }

  return logList;
}


() => {
  let component: LogsComponent;
  let fixture: ComponentFixture<LogsComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [LogsComponent, AnsiPipe],
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(LogsComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should stay at the bottom of the container when new logs arrive', () => {
    const { nativeElement } = component.scrollTarget;
    component.scrollToBottom = true;

    component.containerLogs = addLogsToList([]);
    fixture.detectChanges();
    expect(component.containerLogs.length).toBe(15);
    expect(nativeElement.scrollTop).toEqual(
      nativeElement.scrollHeight - nativeElement.offsetHeight
    );

    component.containerLogs = addLogsToList(component.containerLogs);
    fixture.detectChanges();
    expect(component.containerLogs.length).toBe(30);
    expect(nativeElement.scrollTop).toEqual(
      nativeElement.scrollHeight - nativeElement.offsetHeight
    );
  });

  it('should filter messages based on regex expression', () => {
    component.filterText = '([A-Z])\\w+';
    component.shouldDisplayTimestamp = false;
    component.containerLogs = addLogsToList([]);
    fixture.detectChanges();

    const selectHighlights: DebugElement[] = fixture.debugElement.queryAll(
      By.css('.highlight')
    );
    expect(selectHighlights.length).toEqual(75);
    expect(selectHighlights[0].nativeElement.innerText).toEqual('test');
  });

  it('should filter for positive lookahead regex', () => {
    component.filterText = '(?=Just)';
    component.shouldDisplayTimestamp = false;
    component.containerLogs = addLogsToList([]);
    fixture.detectChanges();

    const selectHighlights: DebugElement[] = fixture.debugElement.queryAll(
      By.css('.highlight')
    );
    expect(selectHighlights.length).toEqual(15);
    expect(selectHighlights[0].nativeElement.innerText).toEqual(
      'Just for test'
    );
  });

  it('should filter case insensitive', () => {
    component.filterText = 'JUST';
    component.shouldDisplayTimestamp = false;
    component.containerLogs = addLogsToList([]);
    fixture.detectChanges();

    const selectHighlights: DebugElement[] = fixture.debugElement.queryAll(
      By.css('.highlight')
    );
    expect(selectHighlights.length).toEqual(15);
    expect(selectHighlights[0].nativeElement.innerText).toEqual('Just');
  });

  it('forward button should wrap search at bottom', () => {
    component.containerLogs = [
      {
        timestamp: '2019-05-06T18:50:06.554540433Z',
        message: 'Test log line 1',
        container: 'test-container',
      },
      {
        timestamp: '2019-05-06T18:59:06.554540433Z',
        message: 'Test log line 2',
        container: 'test-container',
      },
    ];
    component.filterText = 'Test log';
    fixture.detectChanges();

    const prevButton = fixture.debugElement.nativeElement.querySelector(
      '#button-prev'
    );
    const nextButton = fixture.debugElement.nativeElement.querySelector(
      '#button-next'
    );
    const badgeElement: HTMLDivElement = fixture.debugElement.query(
      By.css('.clr-filter-summary')
    ).nativeElement;

    expect(badgeElement.innerText).toBe('1/2 items');
    nextButton.click();

    fixture.whenStable().then(() => {
      const offsetSecondElement = getSelectedHighlightTop();

      fixture.detectChanges();
      expect(badgeElement.innerText).toBe('2/2 items');

      nextButton.click();
      fixture.detectChanges();
      expect(getSelectedHighlightTop()).toBeLessThan(offsetSecondElement); // should roll-up to 1st
      expect(badgeElement.innerText).toBe('1/2 items');

      prevButton.click();
      fixture.detectChanges();
      expect(getSelectedHighlightTop()).toBe(offsetSecondElement); // should come back to 2nd
      expect(badgeElement.innerText).toBe('2/2 items');
    });
  });

  function getSelectedHighlightTop() {
    const nextSelectedElement: HTMLDivElement = fixture.debugElement.query(
      By.css('.highlight-selected')
    ).nativeElement;

    return nextSelectedElement.offsetTop;
  }
}


() => {
    TestBed.configureTestingModule({
      declarations: [LogsComponent, AnsiPipe],
    }).compileComponents();
  }


() => {
    fixture = TestBed.createComponent(LogsComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }


() => {
    const { nativeElement } = component.scrollTarget;
    component.scrollToBottom = true;

    component.containerLogs = addLogsToList([]);
    fixture.detectChanges();
    expect(component.containerLogs.length).toBe(15);
    expect(nativeElement.scrollTop).toEqual(
      nativeElement.scrollHeight - nativeElement.offsetHeight
    );

    component.containerLogs = addLogsToList(component.containerLogs);
    fixture.detectChanges();
    expect(component.containerLogs.length).toBe(30);
    expect(nativeElement.scrollTop).toEqual(
      nativeElement.scrollHeight - nativeElement.offsetHeight
    );
  }


() => {
    component.filterText = '([A-Z])\\w+';
    component.shouldDisplayTimestamp = false;
    component.containerLogs = addLogsToList([]);
    fixture.detectChanges();

    const selectHighlights: DebugElement[] = fixture.debugElement.queryAll(
      By.css('.highlight')
    );
    expect(selectHighlights.length).toEqual(75);
    expect(selectHighlights[0].nativeElement.innerText).toEqual('test');
  }


() => {
    component.filterText = '(?=Just)';
    component.shouldDisplayTimestamp = false;
    component.containerLogs = addLogsToList([]);
    fixture.detectChanges();

    const selectHighlights: DebugElement[] = fixture.debugElement.queryAll(
      By.css('.highlight')
    );
    expect(selectHighlights.length).toEqual(15);
    expect(selectHighlights[0].nativeElement.innerText).toEqual(
      'Just for test'
    );
  }


() => {
    component.filterText = 'JUST';
    component.shouldDisplayTimestamp = false;
    component.containerLogs = addLogsToList([]);
    fixture.detectChanges();

    const selectHighlights: DebugElement[] = fixture.debugElement.queryAll(
      By.css('.highlight')
    );
    expect(selectHighlights.length).toEqual(15);
    expect(selectHighlights[0].nativeElement.innerText).toEqual('Just');
  }


() => {
    component.containerLogs = [
      {
        timestamp: '2019-05-06T18:50:06.554540433Z',
        message: 'Test log line 1',
        container: 'test-container',
      },
      {
        timestamp: '2019-05-06T18:59:06.554540433Z',
        message: 'Test log line 2',
        container: 'test-container',
      },
    ];
    component.filterText = 'Test log';
    fixture.detectChanges();

    const prevButton = fixture.debugElement.nativeElement.querySelector(
      '#button-prev'
    );
    const nextButton = fixture.debugElement.nativeElement.querySelector(
      '#button-next'
    );
    const badgeElement: HTMLDivElement = fixture.debugElement.query(
      By.css('.clr-filter-summary')
    ).nativeElement;

    expect(badgeElement.innerText).toBe('1/2 items');
    nextButton.click();

    fixture.whenStable().then(() => {
      const offsetSecondElement = getSelectedHighlightTop();

      fixture.detectChanges();
      expect(badgeElement.innerText).toBe('2/2 items');

      nextButton.click();
      fixture.detectChanges();
      expect(getSelectedHighlightTop()).toBeLessThan(offsetSecondElement); // should roll-up to 1st
      expect(badgeElement.innerText).toBe('1/2 items');

      prevButton.click();
      fixture.detectChanges();
      expect(getSelectedHighlightTop()).toBe(offsetSecondElement); // should come back to 2nd
      expect(badgeElement.innerText).toBe('2/2 items');
    });
  }


() => {
      const offsetSecondElement = getSelectedHighlightTop();

      fixture.detectChanges();
      expect(badgeElement.innerText).toBe('2/2 items');

      nextButton.click();
      fixture.detectChanges();
      expect(getSelectedHighlightTop()).toBeLessThan(offsetSecondElement); // should roll-up to 1st
      expect(badgeElement.innerText).toBe('1/2 items');

      prevButton.click();
      fixture.detectChanges();
      expect(getSelectedHighlightTop()).toBe(offsetSecondElement); // should come back to 2nd
      expect(badgeElement.innerText).toBe('2/2 items');
    }


(data, idx) => (
                <MediaLink data={data}


class MediaBlock extends Component {
  private readonly items: MediaLinkItem[] = [
    {
      url: 'https://dev.to/koddr/go-fiber-by-examples-working-with-middlewares-and-boilerplates-3p0m',
      website: 'dev.to',
      title:
        '📖 Go Fiber by Examples: Working with middlewares and boilerplates',
      author: 'Vic Shóstak',
      date: 'september 13, 2021',
    },
    {
      url: 'https://dev.to/koddr/go-fiber-by-examples-testing-the-application-1ldf',
      website: 'dev.to',
      title: '📖 Go Fiber by Examples: Testing the application',
      author: 'Vic Shóstak',
      date: 'august 30, 2021',
    },
    {
      url: 'https://dev.to/koddr/go-fiber-by-examples-delving-into-built-in-functions-1p3k',
      website: 'dev.to',
      title: '📖 Go Fiber by Examples: Delving into built-in functions',
      author: 'Vic Shóstak',
      date: 'august 24, 2021',
    },
    {
      url: 'https://dev.to/koddr/go-fiber-by-examples-how-can-the-fiber-web-framework-be-useful-487a',
      website: 'dev.to',
      title:
        '📖 Go Fiber by Examples: How can the Fiber Web Framework be useful?',
      author: 'Vic Shóstak',
      date: 'august 16, 2021',
    },
    {
      url: 'https://dev.to/koddr/build-a-restful-api-on-go-fiber-postgresql-jwt-and-swagger-docs-in-isolated-docker-containers-475j',
      website: 'dev.to',
      title:
        '📖 Build a RESTful API on Go: Fiber, PostgreSQL, JWT and Swagger docs in isolated Docker containers',
      author: 'Vic Shóstak',
      date: 'march 22, 2021',
    },
    {
      url: 'https://dev.to/fenny/getting-started-with-fiber-36b6',
      website: 'dev.to',
      title: 'Getting started with Fiber ⚡',
      author: 'Fenny 🔥',
      date: 'june 10, 2020',
    },
    {
      url: 'https://blog.logrocket.com/express-style-api-go-fiber/',
      website: 'logrocket.com',
      title: 'Building an Express-style API in Go with Fiber',
      author: 'Alexander Nnakwue',
      date: 'june 10, 2020',
    },
    {
      url: 'https://vugt.me/the-road-to-web-based-authentication-with-fiber/',
      website: 'vugt.me',
      title: 'The road to web-based authentication with Fiber ⚡',
      author: 'Thomas van Vugt',
      date: 'May 20, 2020',
    },
    {
      url: 'https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6',
      website: 'dev.to',
      title:
        'Fiber v1.9.6 🔥 How to improve performance by 817% and stay fast, flexible and friendly?',
      author: 'Vic Shóstak',
      date: 'May 12, 2020',
    },
    {
      url: 'https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t',
      website: 'yongweilun.me',
      title:
        '🌎 Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager',
      author: 'Wei Lun',
      date: 'April 25, 2020',
    },
    {
      url: 'https://tutorialedge.net/golang/basic-rest-api-go-fiber/',
      website: 'tutorialedge.net',
      title: 'Building a Basic REST API in Go using Fiber',
      author: 'Elliot Forbes',
      date: 'April 23, 2020',
    },
    {
      url: 'https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9',
      website: 'dev.to',
      title: 'Creating Fast APIs In Go Using Fiber',
      author: 'József Sallai',
      date: 'April 7, 2020',
    },
    {
      url: 'https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1',
      website: 'dev.to',
      title: 'Is switching from Express to Fiber worth it? 🤔',
      author: 'Vic Shóstak',
      date: 'April 1, 2020',
    },
    {
      url: 'https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h',
      website: 'dev.to',
      title: "🚀 Fiber v1.8. What's new, updated and re-thinked?",
      author: 'Vic Shóstak',
      date: 'March 3, 2020',
    },
    {
      url: 'https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf',
      website: 'dev.to',
      title:
        "Fiber released v1.7! 🎉 What's new and is it still fast, flexible and friendly?",
      author: 'Vic Shóstak',
      date: 'February 21, 2020',
    },
    {
      url: 'https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497',
      website: 'dev.to',
      title:
        'Welcome to Fiber — an Express.js styled web framework written in Go with ❤',
      author: 'Vic Shóstak',
      date: 'February 3, 2020',
    },
  ];

  render() {
    return (
      <section className={`generic-block faint reverse ${styles.mediaBlock}`}


render() {
    return (
      <section className={`generic-block faint reverse ${styles.mediaBlock}`}


() => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getTitleText()).toEqual('angular10cms app is running!');
  });

  afterEach(async () => {
    // Assert that there are no errors emitted from the browser
    const logs = await browser.manage().logs().get(logging.Type.BROWSER);
    expect(logs).not.toContain(jasmine.objectContaining({
      level: logging.Level.SEVERE,
    } as logging.Entry));
  });
}


() => {
    page.navigateTo();
    expect(page.getTitleText()).toEqual('angular10cms app is running!');
  }


({ route, navigation }: Props) => {
  const client = useApolloClient()

  const { screenPurpose } = route.params

  const [enteredPIN, setEnteredPIN] = useState("")
  const [helperText, setHelperText] = useState(
    screenPurpose === PinScreenPurpose.SetPin ? translate("PinScreen.setPin") : "",
  )
  const [previousPIN, setPreviousPIN] = useState("")
  const [pinAttempts, setPinAttempts] = useState(0)

  const MAX_PIN_ATTEMPTS = 3

  useEffect(() => {
    ;(async () => {
      setPinAttempts(await KeyStoreWrapper.getPinAttemptsOrZero())
    })()
  }, [])

  const handleCompletedPinForAuthenticatePin = async (newEnteredPIN: string) => {
    if (newEnteredPIN === (await KeyStoreWrapper.getPinOrEmptyString())) {
      KeyStoreWrapper.resetPinAttempts()
      navigation.reset({
        index: 0,
        routes: [{ name: "Primary" }],
      })
      checkClipboard(client)
    } else {
      if (pinAttempts < MAX_PIN_ATTEMPTS - 1) {
        const newPinAttempts = pinAttempts + 1
        KeyStoreWrapper.setPinAttempts(newPinAttempts.toString())
        setPinAttempts(newPinAttempts)
        setEnteredPIN("")
        if (newPinAttempts === MAX_PIN_ATTEMPTS - 1) {
          setHelperText(translate("PinScreen.oneAttemptRemaining"))
        } else {
          const attemptsRemaining = MAX_PIN_ATTEMPTS - newPinAttempts
          setHelperText(translate("PinScreen.attemptsRemaining", { attemptsRemaining }))
        }
      } else {
        setHelperText(translate("PinScreen.tooManyAttempts"))
        await resetDataStore(client)
        await sleep(1000)
        navigation.reset({
          index: 0,
          routes: [{ name: "Primary" }],
        })
      }
    }
  }

  const handleCompletedPinForSetPin = (newEnteredPIN: string) => {
    if (previousPIN.length === 0) {
      setPreviousPIN(newEnteredPIN)
      setHelperText(translate("PinScreen.verifyPin"))
      setEnteredPIN("")
    } else {
      verifyPINCodeMathes(newEnteredPIN)
    }
  }

  const addDigit = (digit: string) => {
    if (enteredPIN.length < 4) {
      const newEnteredPIN = enteredPIN + digit
      setEnteredPIN(newEnteredPIN)

      if (newEnteredPIN.length === 4) {
        if (screenPurpose === PinScreenPurpose.AuthenticatePin) {
          handleCompletedPinForAuthenticatePin(newEnteredPIN)
        } else if (screenPurpose === PinScreenPurpose.SetPin) {
          handleCompletedPinForSetPin(newEnteredPIN)
        }
      }
    }
  }

  const verifyPINCodeMathes = async (newEnteredPIN: string) => {
    if (previousPIN === newEnteredPIN) {
      if (await KeyStoreWrapper.setPin(previousPIN)) {
        KeyStoreWrapper.resetPinAttempts()
        navigation.goBack()
      } else {
        returnToSetPin()
        Alert.alert(translate("PinScreen.storePinFailed"))
      }
    } else {
      returnToSetPin()
    }
  }

  const returnToSetPin = () => {
    setPreviousPIN("")
    setHelperText(translate("PinScreen.setPinFailedMatch"))
    setEnteredPIN("")
  }

  const circleComponentForDigit = (digit: number) => {
    return (
      <View style={styles.circleContainer}>
        <View
          style={enteredPIN.length > digit ? styles.filledCircle : styles.emptyCircle}
        />
      </View>


() => {
    ;(async () => {
      setPinAttempts(await KeyStoreWrapper.getPinAttemptsOrZero())
    })()
  }


async () => {
      setPinAttempts(await KeyStoreWrapper.getPinAttemptsOrZero())
    }


async (newEnteredPIN: string) => {
    if (newEnteredPIN === (await KeyStoreWrapper.getPinOrEmptyString())) {
      KeyStoreWrapper.resetPinAttempts()
      navigation.reset({
        index: 0,
        routes: [{ name: "Primary" }],
      })
      checkClipboard(client)
    } else {
      if (pinAttempts < MAX_PIN_ATTEMPTS - 1) {
        const newPinAttempts = pinAttempts + 1
        KeyStoreWrapper.setPinAttempts(newPinAttempts.toString())
        setPinAttempts(newPinAttempts)
        setEnteredPIN("")
        if (newPinAttempts === MAX_PIN_ATTEMPTS - 1) {
          setHelperText(translate("PinScreen.oneAttemptRemaining"))
        } else {
          const attemptsRemaining = MAX_PIN_ATTEMPTS - newPinAttempts
          setHelperText(translate("PinScreen.attemptsRemaining", { attemptsRemaining }))
        }
      } else {
        setHelperText(translate("PinScreen.tooManyAttempts"))
        await resetDataStore(client)
        await sleep(1000)
        navigation.reset({
          index: 0,
          routes: [{ name: "Primary" }],
        })
      }
    }
  }


(newEnteredPIN: string) => {
    if (previousPIN.length === 0) {
      setPreviousPIN(newEnteredPIN)
      setHelperText(translate("PinScreen.verifyPin"))
      setEnteredPIN("")
    } else {
      verifyPINCodeMathes(newEnteredPIN)
    }
  }


(digit: string) => {
    if (enteredPIN.length < 4) {
      const newEnteredPIN = enteredPIN + digit
      setEnteredPIN(newEnteredPIN)

      if (newEnteredPIN.length === 4) {
        if (screenPurpose === PinScreenPurpose.AuthenticatePin) {
          handleCompletedPinForAuthenticatePin(newEnteredPIN)
        } else if (screenPurpose === PinScreenPurpose.SetPin) {
          handleCompletedPinForSetPin(newEnteredPIN)
        }
      }
    }
  }


async (newEnteredPIN: string) => {
    if (previousPIN === newEnteredPIN) {
      if (await KeyStoreWrapper.setPin(previousPIN)) {
        KeyStoreWrapper.resetPinAttempts()
        navigation.goBack()
      } else {
        returnToSetPin()
        Alert.alert(translate("PinScreen.storePinFailed"))
      }
    } else {
      returnToSetPin()
    }
  }


() => {
    setPreviousPIN("")
    setHelperText(translate("PinScreen.setPinFailedMatch"))
    setEnteredPIN("")
  }


(digit: number) => {
    return (
      <View style={styles.circleContainer}>
        <View
          style={enteredPIN.length > digit ? styles.filledCircle : styles.emptyCircle}
        />
      </View>


(digit: string) => {
    return (
      <View style={styles.pinPadButtonContainer}>
        <Button
          buttonStyle={styles.pinPadButton}
          titleStyle={styles.pinPadButtonTitle}
          title={digit}
          onPress={()


type Props = {
  route: {
    params: {
      screenPurpose: PinScreenPurpose
    }
  }
  navigation: any
}


function usePopoverContext(component: string) {
  let context = inject(PopoverContext, null)
  if (context === null) {
    let err = new Error(`<${component} /> is missing a parent <${Popover.name} /> component.`)
    if (Error.captureStackTrace) Error.captureStackTrace(err, usePopoverContext)
    throw err
  }
  return context
}


function usePopoverGroupContext() {
  return inject(PopoverGroupContext, null)
}


function usePopoverPanelContext() {
  return inject(PopoverPanelContext, null)
}


function isFocusWithinPopoverGroup() {
      return (
        groupContext?.isFocusWithinPopoverGroup() ??
        (dom(button)?.contains(document.activeElement) ||
          dom(panel)?.contains(document.activeElement))
      )
    }


function handleKeyDown(event: KeyboardEvent) {
      if (isWithinPanel) {
        if (api.popoverState.value === PopoverStates.Closed) return
        switch (event.key) {
          case Keys.Space:
          case Keys.Enter:
            event.preventDefault() // Prevent triggering a *click* event
            // @ts-expect-error
            event.target.click?.()
            api.closePopover()
            dom(api.button)?.focus() // Re-focus the original opening Button
            break
        }
      } else {
        switch (event.key) {
          case Keys.Space:
          case Keys.Enter:
            event.preventDefault() // Prevent triggering a *click* event
            event.stopPropagation()
            if (api.popoverState.value === PopoverStates.Closed) closeOthers?.(api.buttonId)
            api.togglePopover()
            break

          case Keys.Escape:
            if (api.popoverState.value !== PopoverStates.Open) return closeOthers?.(api.buttonId)
            if (!dom(api.button)) return
            if (!dom(api.button)?.contains(document.activeElement)) return
            event.preventDefault()
            event.stopPropagation()
            api.closePopover()
            break

          case Keys.Tab:
            if (api.popoverState.value !== PopoverStates.Open) return
            if (!api.panel) return
            if (!api.button) return

            // TODO: Revisit when handling Tab/Shift+Tab when using Portal's
            if (event.shiftKey) {
              // Check if the last focused element exists, and check that it is not inside button or panel itself
              if (!previousActiveElementRef.value) return
              if (dom(api.button)?.contains(previousActiveElementRef.value)) return
              if (dom(api.panel)?.contains(previousActiveElementRef.value)) return

              // Check if the last focused element is *after* the button in the DOM
              let focusableElements = getFocusableElements()
              let previousIdx = focusableElements.indexOf(
                previousActiveElementRef.value as HTMLElement
              )
              let buttonIdx = focusableElements.indexOf(dom(api.button)!)
              if (buttonIdx > previousIdx) return

              event.preventDefault()
              event.stopPropagation()

              focusIn(dom(api.panel)!, Focus.Last)
            } else {
              event.preventDefault()
              event.stopPropagation()

              focusIn(dom(api.panel)!, Focus.First)
            }

            break
        }
      }
    }


function handleKeyUp(event: KeyboardEvent) {
      if (isWithinPanel) return
      if (event.key === Keys.Space) {
        // Required for firefox, event.preventDefault() in handleKeyDown for
        // the Space key doesn't cancel the handleKeyUp, which in turn
        // triggers a *click*.
        event.preventDefault()
      }
      if (api.popoverState.value !== PopoverStates.Open) return
      if (!api.panel) return
      if (!api.button) return

      // TODO: Revisit when handling Tab/Shift+Tab when using Portal's
      switch (event.key) {
        case Keys.Tab:
          // Check if the last focused element exists, and check that it is not inside button or panel itself
          if (!previousActiveElementRef.value) return
          if (dom(api.button)?.contains(previousActiveElementRef.value)) return
          if (dom(api.panel)?.contains(previousActiveElementRef.value)) return

          // Check if the last focused element is *after* the button in the DOM
          let focusableElements = getFocusableElements()
          let previousIdx = focusableElements.indexOf(previousActiveElementRef.value as HTMLElement)
          let buttonIdx = focusableElements.indexOf(dom(api.button)!)
          if (buttonIdx > previousIdx) return

          event.preventDefault()
          event.stopPropagation()
          focusIn(dom(api.panel)!, Focus.Last)
          break
      }
    }


function handleClick() {
      if (props.disabled) return
      if (isWithinPanel) {
        api.closePopover()
        dom(api.button)?.focus() // Re-focus the original opening Button
      } else {
        if (api.popoverState.value === PopoverStates.Closed) closeOthers?.(api.buttonId)
        dom(api.button)?.focus()
        api.togglePopover()
      }
    }


function handleClick() {
      api.closePopover()
    }


function handleKeyDown(event: KeyboardEvent) {
      switch (event.key) {
        case Keys.Escape:
          if (api.popoverState.value !== PopoverStates.Open) return
          if (!dom(api.panel)) return
          if (!dom(api.panel)?.contains(document.activeElement)) return
          event.preventDefault()
          event.stopPropagation()
          api.closePopover()
          dom(api.button)?.focus()
          break
      }
    }


function unregisterPopover(registerBag: PopoverRegisterBag) {
      let idx = popovers.value.indexOf(registerBag)
      if (idx !== -1) popovers.value.splice(idx, 1)
    }


function registerPopover(registerBag: PopoverRegisterBag) {
      popovers.value.push(registerBag)
      return () => {
        unregisterPopover(registerBag)
      }
    }


function isFocusWithinPopoverGroup() {
      let element = document.activeElement as HTMLElement

      if (dom(groupRef)?.contains(element)) return true

      // Check if the focus is in one of the button or panel elements. This is important in case you are rendering inside a Portal.
      return popovers.value.some((bag) => {
        return (
          document.getElementById(bag.buttonId)?.contains(element) ||
          document.getElementById(bag.panelId)?.contains(element)
        )
      })
    }


function closeOthers(buttonId: string) {
      for (let popover of popovers.value) {
        if (popover.buttonId !== buttonId) popover.close()
      }
    }


() => {
          if (!focusableElement) return dom(api.button)
          if (focusableElement instanceof HTMLElement) return focusableElement
          if (focusableElement.value instanceof HTMLElement) return dom(focusableElement)

          return dom(api.button)
        }


() =>
        match(popoverState.value, {
          [PopoverStates.Open]: State.Open,
          [PopoverStates.Closed]: State.Closed,
        })


() => registerPopover?.(registerBag)


() => {
        if (popoverState.value !== PopoverStates.Open) return
        if (isFocusWithinPopoverGroup()) return
        if (!button) return
        if (!panel) return

        api.closePopover()
      }


(event, target) => {
      if (popoverState.value !== PopoverStates.Open) return

      api.closePopover()

      if (!isFocusableElement(target, FocusableMode.Loose)) {
        event.preventDefault()
        dom(button)?.focus()
      }
    }


() => {
      let slot = { open: popoverState.value === PopoverStates.Open, close: api.close }
      return render({ props, slot, slots, attrs, name: 'Popover' })
    }


() => {
        previousActiveElementRef.value = activeElementRef.value
        activeElementRef.value = document.activeElement
      }


() => {
        api.button.value = elementRef.value
      }


() => ({ as: props.as, type: attrs.type })


() => {
      let slot = { open: api.popoverState.value === PopoverStates.Open }
      let propsWeControl = isWithinPanel
        ? {
            ref: elementRef,
            type: type.value,
            onKeydown: handleKeyDown,
            onClick: handleClick,
          }
        : {
            ref: elementRef,
            id: api.buttonId,
            type: type.value,
            'aria-expanded': props.disabled
              ? undefined
              : api.popoverState.value === PopoverStates.Open,
            'aria-controls': dom(api.panel) ? api.panelId : undefined,
            disabled: props.disabled ? true : undefined,
            onKeydown: handleKeyDown,
            onKeyup: handleKeyUp,
            onClick: handleClick,
          }

      return render({
        props: { ...props, ...propsWeControl },
        slot,
        attrs: attrs,
        slots: slots,
        name: 'PopoverButton',
      })
    }


() => {
      if (usesOpenClosedState !== null) {
        return usesOpenClosedState.value === State.Open
      }

      return api.popoverState.value === PopoverStates.Open
    }


() => {
      let slot = { open: api.popoverState.value === PopoverStates.Open }
      let propsWeControl = {
        id,
        'aria-hidden': true,
        onClick: handleClick,
      }

      return render({
        props: { ...props, ...propsWeControl },
        slot,
        attrs,
        slots,
        features: Features.RenderStrategy | Features.Static,
        visible: visible.value,
        name: 'PopoverOverlay',
      })
    }


() => {
      api.panel.value = null
    }


() => {
      if (!focus) return
      if (api.popoverState.value !== PopoverStates.Open) return
      if (!api.panel) return

      let activeElement = document.activeElement as HTMLElement
      if (dom(api.panel)?.contains(activeElement)) return // Already focused within Dialog

      focusIn(dom(api.panel)!, Focus.First)
    }


(event: KeyboardEvent) => {
      if (api.popoverState.value !== PopoverStates.Open) return
      if (!dom(api.panel)) return

      if (event.key !== Keys.Tab) return
      if (!document.activeElement) return
      if (!dom(api.panel)?.contains(document.activeElement)) return

      // We will take-over the default tab behaviour so that we have a bit
      // control over what is focused next. It will behave exactly the same,
      // but it will also "fix" some issues based on whether you are using a
      // Portal or not.
      event.preventDefault()

      let result = focusIn(dom(api.panel)!, event.shiftKey ? Focus.Previous : Focus.Next)

      if (result === FocusResult.Underflow) {
        return dom(api.button)?.focus()
      } else if (result === FocusResult.Overflow) {
        if (!dom(api.button)) return

        let elements = getFocusableElements()
        let buttonIdx = elements.indexOf(dom(api.button)!)

        let nextElements = elements
          .splice(buttonIdx + 1) // Elements after button
          .filter((element) => !dom(api.panel)?.contains(element)) // Ignore items in panel

        // Try to focus the next element, however it could fail if we are in a
        // Portal that happens to be the very last one in the DOM. In that
        // case we would Error (because nothing after the button is
        // focusable). Therefore we will try and focus the very first item in
        // the document.body.
        if (focusIn(nextElements, Focus.First) === FocusResult.Error) {
          focusIn(document.body, Focus.First)
        }
      }
    }


(element) => !dom(api.panel)?.contains(element)


() => {
        if (!focus) return
        if (api.popoverState.value !== PopoverStates.Open) return
        if (!dom(api.panel)) return
        if (dom(api.panel)?.contains(document.activeElement as HTMLElement)) return
        api.closePopover()
      }


() => {
      let slot = {
        open: api.popoverState.value === PopoverStates.Open,
        close: api.close,
      }

      let propsWeControl = {
        ref: api.panel,
        id: api.panelId,
        onKeydown: handleKeyDown,
      }

      return render({
        props: { ...props, ...propsWeControl },
        slot,
        attrs,
        slots,
        features: Features.RenderStrategy | Features.Static,
        visible: visible.value,
        name: 'PopoverPanel',
      })
    }


() => {
        unregisterPopover(registerBag)
      }


(bag) => {
        return (
          document.getElementById(bag.buttonId)?.contains(element) ||
          document.getElementById(bag.panelId)?.contains(element)
        )
      }


() => {
      let propsWeControl = { ref: groupRef }

      return render({
        props: { ...props, ...propsWeControl },
        slot: {},
        attrs,
        slots,
        name: 'PopoverGroup',
      })
    }


interface StateDefinition {
  // State
  popoverState: Ref<PopoverStates>
  button: Ref<HTMLElement | null>
  buttonId: string
  panel: Ref<HTMLElement | null>
  panelId: string

  // State mutators
  togglePopover(): void
  closePopover(): void

  // Exposed functions
  close(focusableElement: HTMLElement | Ref<HTMLElement | null>): void
}


interface PopoverRegisterBag {
  buttonId: string
  panelId: string
  close(): void
}


enum PopoverStates {
  Open,
  Closed,
}


setup(props, { slots, attrs }) {
    let buttonId = `headlessui-popover-button-${useId()}`
    let panelId = `headlessui-popover-panel-${useId()}`

    let popoverState = ref<StateDefinition['popoverState']['value']>(PopoverStates.Closed)
    let button = ref<StateDefinition['button']['value']>(null)
    let panel = ref<StateDefinition['panel']['value']>(null)

    let api = {
      popoverState,
      buttonId,
      panelId,
      panel,
      button,
      togglePopover() {
        popoverState.value = match(popoverState.value, {
          [PopoverStates.Open]: PopoverStates.Closed,
          [PopoverStates.Closed]: PopoverStates.Open,
        })
      },
      closePopover() {
        if (popoverState.value === PopoverStates.Closed) return
        popoverState.value = PopoverStates.Closed
      },
      close(focusableElement: HTMLElement | Ref<HTMLElement | null>) {
        api.closePopover()

        let restoreElement = (() => {
          if (!focusableElement) return dom(api.button)
          if (focusableElement instanceof HTMLElement) return focusableElement
          if (focusableElement.value instanceof HTMLElement) return dom(focusableElement)

          return dom(api.button)
        })()

        restoreElement?.focus()
      },
    } as StateDefinition

    provide(PopoverContext, api)
    useOpenClosedProvider(
      computed(() =>
        match(popoverState.value, {
          [PopoverStates.Open]: State.Open,
          [PopoverStates.Closed]: State.Closed,
        })
      )
    )

    let registerBag = {
      buttonId,
      panelId,
      close() {
        api.closePopover()
      },
    }

    let groupContext = usePopoverGroupContext()
    let registerPopover = groupContext?.registerPopover

    function isFocusWithinPopoverGroup() {
      return (
        groupContext?.isFocusWithinPopoverGroup() ??
        (dom(button)?.contains(document.activeElement) ||
          dom(panel)?.contains(document.activeElement))
      )
    }

    watchEffect(() => registerPopover?.(registerBag))

    // Handle focus out
    useWindowEvent(
      'focus',
      () => {
        if (popoverState.value !== PopoverStates.Open) return
        if (isFocusWithinPopoverGroup()) return
        if (!button) return
        if (!panel) return

        api.closePopover()
      },
      true
    )

    // Handle outside click
    useOutsideClick([button, panel], (event, target) => {
      if (popoverState.value !== PopoverStates.Open) return

      api.closePopover()

      if (!isFocusableElement(target, FocusableMode.Loose)) {
        event.preventDefault()
        dom(button)?.focus()
      }
    })

    return () => {
      let slot = { open: popoverState.value === PopoverStates.Open, close: api.close }
      return render({ props, slot, slots, attrs, name: 'Popover' })
    }
  }


togglePopover() {
        popoverState.value = match(popoverState.value, {
          [PopoverStates.Open]: PopoverStates.Closed,
          [PopoverStates.Closed]: PopoverStates.Open,
        })
      }


closePopover() {
        if (popoverState.value === PopoverStates.Closed) return
        popoverState.value = PopoverStates.Closed
      }


close(focusableElement: HTMLElement | Ref<HTMLElement | null>) {
        api.closePopover()

        let restoreElement = (() => {
          if (!focusableElement) return dom(api.button)
          if (focusableElement instanceof HTMLElement) return focusableElement
          if (focusableElement.value instanceof HTMLElement) return dom(focusableElement)

          return dom(api.button)
        })()

        restoreElement?.focus()
      }


close() {
        api.closePopover()
      }


setup(props, { attrs, slots }) {
    let api = usePopoverContext('PopoverButton')

    let groupContext = usePopoverGroupContext()
    let closeOthers = groupContext?.closeOthers

    let panelContext = usePopoverPanelContext()
    let isWithinPanel = panelContext === null ? false : panelContext === api.panelId

    // TODO: Revisit when handling Tab/Shift+Tab when using Portal's
    let activeElementRef = ref<Element | null>(null)
    let previousActiveElementRef = ref<Element | null>(
      typeof window === 'undefined' ? null : document.activeElement
    )

    useWindowEvent(
      'focus',
      () => {
        previousActiveElementRef.value = activeElementRef.value
        activeElementRef.value = document.activeElement
      },
      true
    )

    let elementRef = ref(null)

    if (!isWithinPanel) {
      watchEffect(() => {
        api.button.value = elementRef.value
      })
    }

    let type = useResolveButtonType(
      computed(() => ({ as: props.as, type: attrs.type })),
      elementRef
    )

    function handleKeyDown(event: KeyboardEvent) {
      if (isWithinPanel) {
        if (api.popoverState.value === PopoverStates.Closed) return
        switch (event.key) {
          case Keys.Space:
          case Keys.Enter:
            event.preventDefault() // Prevent triggering a *click* event
            // @ts-expect-error
            event.target.click?.()
            api.closePopover()
            dom(api.button)?.focus() // Re-focus the original opening Button
            break
        }
      } else {
        switch (event.key) {
          case Keys.Space:
          case Keys.Enter:
            event.preventDefault() // Prevent triggering a *click* event
            event.stopPropagation()
            if (api.popoverState.value === PopoverStates.Closed) closeOthers?.(api.buttonId)
            api.togglePopover()
            break

          case Keys.Escape:
            if (api.popoverState.value !== PopoverStates.Open) return closeOthers?.(api.buttonId)
            if (!dom(api.button)) return
            if (!dom(api.button)?.contains(document.activeElement)) return
            event.preventDefault()
            event.stopPropagation()
            api.closePopover()
            break

          case Keys.Tab:
            if (api.popoverState.value !== PopoverStates.Open) return
            if (!api.panel) return
            if (!api.button) return

            // TODO: Revisit when handling Tab/Shift+Tab when using Portal's
            if (event.shiftKey) {
              // Check if the last focused element exists, and check that it is not inside button or panel itself
              if (!previousActiveElementRef.value) return
              if (dom(api.button)?.contains(previousActiveElementRef.value)) return
              if (dom(api.panel)?.contains(previousActiveElementRef.value)) return

              // Check if the last focused element is *after* the button in the DOM
              let focusableElements = getFocusableElements()
              let previousIdx = focusableElements.indexOf(
                previousActiveElementRef.value as HTMLElement
              )
              let buttonIdx = focusableElements.indexOf(dom(api.button)!)
              if (buttonIdx > previousIdx) return

              event.preventDefault()
              event.stopPropagation()

              focusIn(dom(api.panel)!, Focus.Last)
            } else {
              event.preventDefault()
              event.stopPropagation()

              focusIn(dom(api.panel)!, Focus.First)
            }

            break
        }
      }
    }

    function handleKeyUp(event: KeyboardEvent) {
      if (isWithinPanel) return
      if (event.key === Keys.Space) {
        // Required for firefox, event.preventDefault() in handleKeyDown for
        // the Space key doesn't cancel the handleKeyUp, which in turn
        // triggers a *click*.
        event.preventDefault()
      }
      if (api.popoverState.value !== PopoverStates.Open) return
      if (!api.panel) return
      if (!api.button) return

      // TODO: Revisit when handling Tab/Shift+Tab when using Portal's
      switch (event.key) {
        case Keys.Tab:
          // Check if the last focused element exists, and check that it is not inside button or panel itself
          if (!previousActiveElementRef.value) return
          if (dom(api.button)?.contains(previousActiveElementRef.value)) return
          if (dom(api.panel)?.contains(previousActiveElementRef.value)) return

          // Check if the last focused element is *after* the button in the DOM
          let focusableElements = getFocusableElements()
          let previousIdx = focusableElements.indexOf(previousActiveElementRef.value as HTMLElement)
          let buttonIdx = focusableElements.indexOf(dom(api.button)!)
          if (buttonIdx > previousIdx) return

          event.preventDefault()
          event.stopPropagation()
          focusIn(dom(api.panel)!, Focus.Last)
          break
      }
    }

    function handleClick() {
      if (props.disabled) return
      if (isWithinPanel) {
        api.closePopover()
        dom(api.button)?.focus() // Re-focus the original opening Button
      } else {
        if (api.popoverState.value === PopoverStates.Closed) closeOthers?.(api.buttonId)
        dom(api.button)?.focus()
        api.togglePopover()
      }
    }

    return () => {
      let slot = { open: api.popoverState.value === PopoverStates.Open }
      let propsWeControl = isWithinPanel
        ? {
            ref: elementRef,
            type: type.value,
            onKeydown: handleKeyDown,
            onClick: handleClick,
          }
        : {
            ref: elementRef,
            id: api.buttonId,
            type: type.value,
            'aria-expanded': props.disabled
              ? undefined
              : api.popoverState.value === PopoverStates.Open,
            'aria-controls': dom(api.panel) ? api.panelId : undefined,
            disabled: props.disabled ? true : undefined,
            onKeydown: handleKeyDown,
            onKeyup: handleKeyUp,
            onClick: handleClick,
          }

      return render({
        props: { ...props, ...propsWeControl },
        slot,
        attrs: attrs,
        slots: slots,
        name: 'PopoverButton',
      })
    }
  }


setup(props, { attrs, slots }) {
    let api = usePopoverContext('PopoverOverlay')
    let id = `headlessui-popover-overlay-${useId()}`

    let usesOpenClosedState = useOpenClosed()
    let visible = computed(() => {
      if (usesOpenClosedState !== null) {
        return usesOpenClosedState.value === State.Open
      }

      return api.popoverState.value === PopoverStates.Open
    })

    function handleClick() {
      api.closePopover()
    }

    return () => {
      let slot = { open: api.popoverState.value === PopoverStates.Open }
      let propsWeControl = {
        id,
        'aria-hidden': true,
        onClick: handleClick,
      }

      return render({
        props: { ...props, ...propsWeControl },
        slot,
        attrs,
        slots,
        features: Features.RenderStrategy | Features.Static,
        visible: visible.value,
        name: 'PopoverOverlay',
      })
    }
  }


setup(props, { attrs, slots }) {
    let { focus } = props
    let api = usePopoverContext('PopoverPanel')

    provide(PopoverPanelContext, api.panelId)

    onUnmounted(() => {
      api.panel.value = null
    })

    // Move focus within panel
    watchEffect(() => {
      if (!focus) return
      if (api.popoverState.value !== PopoverStates.Open) return
      if (!api.panel) return

      let activeElement = document.activeElement as HTMLElement
      if (dom(api.panel)?.contains(activeElement)) return // Already focused within Dialog

      focusIn(dom(api.panel)!, Focus.First)
    })

    // Handle Tab / Shift+Tab focus positioning
    useWindowEvent('keydown', (event: KeyboardEvent) => {
      if (api.popoverState.value !== PopoverStates.Open) return
      if (!dom(api.panel)) return

      if (event.key !== Keys.Tab) return
      if (!document.activeElement) return
      if (!dom(api.panel)?.contains(document.activeElement)) return

      // We will take-over the default tab behaviour so that we have a bit
      // control over what is focused next. It will behave exactly the same,
      // but it will also "fix" some issues based on whether you are using a
      // Portal or not.
      event.preventDefault()

      let result = focusIn(dom(api.panel)!, event.shiftKey ? Focus.Previous : Focus.Next)

      if (result === FocusResult.Underflow) {
        return dom(api.button)?.focus()
      } else if (result === FocusResult.Overflow) {
        if (!dom(api.button)) return

        let elements = getFocusableElements()
        let buttonIdx = elements.indexOf(dom(api.button)!)

        let nextElements = elements
          .splice(buttonIdx + 1) // Elements after button
          .filter((element) => !dom(api.panel)?.contains(element)) // Ignore items in panel

        // Try to focus the next element, however it could fail if we are in a
        // Portal that happens to be the very last one in the DOM. In that
        // case we would Error (because nothing after the button is
        // focusable). Therefore we will try and focus the very first item in
        // the document.body.
        if (focusIn(nextElements, Focus.First) === FocusResult.Error) {
          focusIn(document.body, Focus.First)
        }
      }
    })

    // Handle focus out when we are in special "focus" mode
    useWindowEvent(
      'focus',
      () => {
        if (!focus) return
        if (api.popoverState.value !== PopoverStates.Open) return
        if (!dom(api.panel)) return
        if (dom(api.panel)?.contains(document.activeElement as HTMLElement)) return
        api.closePopover()
      },
      true
    )

    let usesOpenClosedState = useOpenClosed()
    let visible = computed(() => {
      if (usesOpenClosedState !== null) {
        return usesOpenClosedState.value === State.Open
      }

      return api.popoverState.value === PopoverStates.Open
    })

    function handleKeyDown(event: KeyboardEvent) {
      switch (event.key) {
        case Keys.Escape:
          if (api.popoverState.value !== PopoverStates.Open) return
          if (!dom(api.panel)) return
          if (!dom(api.panel)?.contains(document.activeElement)) return
          event.preventDefault()
          event.stopPropagation()
          api.closePopover()
          dom(api.button)?.focus()
          break
      }
    }

    return () => {
      let slot = {
        open: api.popoverState.value === PopoverStates.Open,
        close: api.close,
      }

      let propsWeControl = {
        ref: api.panel,
        id: api.panelId,
        onKeydown: handleKeyDown,
      }

      return render({
        props: { ...props, ...propsWeControl },
        slot,
        attrs,
        slots,
        features: Features.RenderStrategy | Features.Static,
        visible: visible.value,
        name: 'PopoverPanel',
      })
    }
  }


setup(props, { attrs, slots }) {
    let groupRef = ref<HTMLElement | null>(null)
    let popovers = ref<PopoverRegisterBag[]>([])

    function unregisterPopover(registerBag: PopoverRegisterBag) {
      let idx = popovers.value.indexOf(registerBag)
      if (idx !== -1) popovers.value.splice(idx, 1)
    }

    function registerPopover(registerBag: PopoverRegisterBag) {
      popovers.value.push(registerBag)
      return () => {
        unregisterPopover(registerBag)
      }
    }

    function isFocusWithinPopoverGroup() {
      let element = document.activeElement as HTMLElement

      if (dom(groupRef)?.contains(element)) return true

      // Check if the focus is in one of the button or panel elements. This is important in case you are rendering inside a Portal.
      return popovers.value.some((bag) => {
        return (
          document.getElementById(bag.buttonId)?.contains(element) ||
          document.getElementById(bag.panelId)?.contains(element)
        )
      })
    }

    function closeOthers(buttonId: string) {
      for (let popover of popovers.value) {
        if (popover.buttonId !== buttonId) popover.close()
      }
    }

    provide(PopoverGroupContext, {
      registerPopover,
      unregisterPopover,
      isFocusWithinPopoverGroup,
      closeOthers,
    })

    return () => {
      let propsWeControl = { ref: groupRef }

      return render({
        props: { ...props, ...propsWeControl },
        slot: {},
        attrs,
        slots,
        name: 'PopoverGroup',
      })
    }
  }


export async function createTokenAccountTransaction({
  connection,
  wallet,
  mintPublicKey,
}: {
  connection: Connection;
  wallet: WalletAdapter;
  mintPublicKey: PublicKey;
}): Promise<{
  transaction: Transaction;
  newAccountPubkey: PublicKey;
}> {
  const ata = await Token.getAssociatedTokenAddress(
    ASSOCIATED_TOKEN_PROGRAM_ID,
    TOKEN_PROGRAM_ID,
    mintPublicKey,
    wallet.publicKey,
  );
  const transaction = new Transaction();
  transaction.add(
    Token.createAssociatedTokenAccountInstruction(
      ASSOCIATED_TOKEN_PROGRAM_ID,
      TOKEN_PROGRAM_ID,
      mintPublicKey,
      ata,
      wallet.publicKey,
      wallet.publicKey,
    ),
  );
  return {
    transaction,
    newAccountPubkey: ata,
  };
}


export async function settleFunds({
  market,
  openOrders,
  connection,
  wallet,
  baseCurrencyAccount,
  quoteCurrencyAccount,
  sendNotification = true,
  usdcRef = undefined,
  usdtRef = undefined,
}: {
  market: Market;
  openOrders: OpenOrders;
  connection: Connection;
  wallet: WalletAdapter;
  baseCurrencyAccount: TokenAccount;
  quoteCurrencyAccount: TokenAccount;
  sendNotification?: boolean;
  usdcRef?: PublicKey;
  usdtRef?: PublicKey;
}): Promise<string | undefined> {
  if (
    !market ||
    !wallet ||
    !connection ||
    !openOrders ||
    (!baseCurrencyAccount && !quoteCurrencyAccount)
  ) {
    if (sendNotification) {
      notify({ message: 'Not connected' });
    }
    return;
  }

  let createAccountTransaction: Transaction | undefined;
  let baseCurrencyAccountPubkey = baseCurrencyAccount?.pubkey;
  let quoteCurrencyAccountPubkey = quoteCurrencyAccount?.pubkey;

  if (!baseCurrencyAccountPubkey) {
    const result = await createTokenAccountTransaction({
      connection,
      wallet,
      mintPublicKey: market.baseMintAddress,
    });
    baseCurrencyAccountPubkey = result?.newAccountPubkey;
    createAccountTransaction = result?.transaction;
  }
  if (!quoteCurrencyAccountPubkey) {
    const result = await createTokenAccountTransaction({
      connection,
      wallet,
      mintPublicKey: market.quoteMintAddress,
    });
    quoteCurrencyAccountPubkey = result?.newAccountPubkey;
    createAccountTransaction = result?.transaction;
  }
  let referrerQuoteWallet: PublicKey | null = null;
  if (market.supportsReferralFees) {
    const usdt = TOKEN_MINTS.find(({ name }) => name === 'USDT');
    const usdc = TOKEN_MINTS.find(({ name }) => name === 'USDC');
    if (usdtRef && usdt && market.quoteMintAddress.equals(usdt.address)) {
      referrerQuoteWallet = usdtRef;
    } else if (
      usdcRef &&
      usdc &&
      market.quoteMintAddress.equals(usdc.address)
    ) {
      referrerQuoteWallet = usdcRef;
    }
  }
  const {
    transaction: settleFundsTransaction,
    signers: settleFundsSigners,
  } = await market.makeSettleFundsTransaction(
    connection,
    openOrders,
    baseCurrencyAccountPubkey,
    quoteCurrencyAccountPubkey,
    referrerQuoteWallet,
  );

  let transaction = mergeTransactions([
    createAccountTransaction,
    settleFundsTransaction,
  ]);

  return await sendTransaction({
    transaction,
    signers: settleFundsSigners,
    wallet,
    connection,
    sendingMessage: 'Settling funds...',
    sendNotification,
  });
}


export async function settleAllFunds({
  connection,
  wallet,
  tokenAccounts,
  markets,
  selectedTokenAccounts,
}: {
  connection: Connection;
  wallet: WalletAdapter;
  tokenAccounts: TokenAccount[];
  markets: Market[];
  selectedTokenAccounts?: SelectedTokenAccounts;
}) {
  if (!markets || !wallet || !connection || !tokenAccounts) {
    return;
  }

  const programIds: PublicKey[] = [];
  markets
    .reduce((cumulative, m) => {
      // @ts-ignore
      cumulative.push(m._programId);
      return cumulative;
    }, [])
    .forEach((programId) => {
      if (!programIds.find((p) => p.equals(programId))) {
        programIds.push(programId);
      }
    });

  const getOpenOrdersAccountsForProgramId = async (programId) => {
    const openOrdersAccounts = await OpenOrders.findForOwner(
      connection,
      wallet.publicKey,
      programId,
    );
    return openOrdersAccounts.filter(
      (openOrders) =>
        openOrders.baseTokenFree.toNumber() ||
        openOrders.quoteTokenFree.toNumber(),
    );
  };

  const openOrdersAccountsForProgramIds = await Promise.all(
    programIds.map((programId) => getOpenOrdersAccountsForProgramId(programId)),
  );
  const openOrdersAccounts = openOrdersAccountsForProgramIds.reduce(
    (accounts, current) => accounts.concat(current),
    [],
  );

  const settleTransactions = (
    await Promise.all(
      openOrdersAccounts.map((openOrdersAccount) => {
        const market = markets.find((m) =>
          // @ts-ignore
          m._decoded?.ownAddress?.equals(openOrdersAccount.market),
        );
        if (
          openOrdersAccount.baseTokenFree.isZero() &&
          openOrdersAccount.quoteTokenFree.isZero()
        ) {
          // nothing to settle for this market.
          return null;
        }
        const baseMint = market?.baseMintAddress;
        const quoteMint = market?.quoteMintAddress;

        const selectedBaseTokenAccount = getSelectedTokenAccountForMint(
          tokenAccounts,
          baseMint,
          baseMint &&
            selectedTokenAccounts &&
            selectedTokenAccounts[baseMint.toBase58()],
        )?.pubkey;
        const selectedQuoteTokenAccount = getSelectedTokenAccountForMint(
          tokenAccounts,
          quoteMint,
          quoteMint &&
            selectedTokenAccounts &&
            selectedTokenAccounts[quoteMint.toBase58()],
        )?.pubkey;
        if (!selectedBaseTokenAccount || !selectedQuoteTokenAccount) {
          return null;
        }
        return (
          market &&
          market.makeSettleFundsTransaction(
            connection,
            openOrdersAccount,
            selectedBaseTokenAccount,
            selectedQuoteTokenAccount,
          )
        );
      }),
    )
  ).filter(
    (
      x,
    ): x is {
      signers: Account[];
      transaction: Transaction;
      payer: PublicKey;
    } => !!x,
  );
  if (!settleTransactions || settleTransactions.length === 0) return;

  const transactions = settleTransactions.slice(0, 4).map((t) => t.transaction);
  const signers: Array<Account> = [];
  settleTransactions
    .reduce((cumulative: Array<Account>, t) => cumulative.concat(t.signers), [])
    .forEach((signer) => {
      if (!signers.find((s) => s.publicKey.equals(signer.publicKey))) {
        signers.push(signer);
      }
    });

  const transaction = mergeTransactions(transactions);

  return await sendTransaction({
    transaction,
    signers,
    wallet,
    connection,
  });
}


export async function cancelOrder(params: {
  market: Market;
  connection: Connection;
  wallet: WalletAdapter;
  order: Order;
}) {
  return cancelOrders({ ...params, orders: [params.order] });
}


export async function cancelOrders({
  market,
  wallet,
  connection,
  orders,
}: {
  market: Market;
  wallet: WalletAdapter;
  connection: Connection;
  orders: Order[];
}) {
  const transaction = market.makeMatchOrdersTransaction(5);
  orders.forEach((order) => {
    transaction.add(
      market.makeCancelOrderInstruction(connection, wallet.publicKey, order),
    );
  });
  transaction.add(market.makeMatchOrdersTransaction(5));
  return await sendTransaction({
    transaction,
    wallet,
    connection,
    sendingMessage: 'Sending cancel...',
  });
}


export async function placeOrder({
  side,
  price,
  size,
  orderType,
  market,
  connection,
  wallet,
  baseCurrencyAccount,
  quoteCurrencyAccount,
  feeDiscountPubkey = undefined,
}: {
  side: 'buy' | 'sell';
  price: number;
  size: number;
  orderType: 'ioc' | 'postOnly' | 'limit';
  market: Market | undefined | null;
  connection: Connection;
  wallet: WalletAdapter;
  baseCurrencyAccount: PublicKey | undefined;
  quoteCurrencyAccount: PublicKey | undefined;
  feeDiscountPubkey: PublicKey | undefined;
}) {
  let formattedMinOrderSize =
    market?.minOrderSize?.toFixed(getDecimalCount(market.minOrderSize)) ||
    market?.minOrderSize;
  let formattedTickSize =
    market?.tickSize?.toFixed(getDecimalCount(market.tickSize)) ||
    market?.tickSize;
  const isIncrement = (num, step) =>
    Math.abs((num / step) % 1) < 1e-5 ||
    Math.abs(((num / step) % 1) - 1) < 1e-5;
  if (isNaN(price)) {
    notify({ message: 'Invalid price', type: 'error' });
    return;
  }
  if (isNaN(size)) {
    notify({ message: 'Invalid size', type: 'error' });
    return;
  }
  if (!wallet || !wallet.publicKey) {
    notify({ message: 'Connect wallet', type: 'error' });
    return;
  }
  if (!market) {
    notify({ message: 'Invalid  market', type: 'error' });
    return;
  }
  if (!isIncrement(size, market.minOrderSize)) {
    notify({
      message: `Size must be an increment of ${formattedMinOrderSize}`,
      type: 'error',
    });
    return;
  }
  if (size < market.minOrderSize) {
    notify({ message: 'Size too small', type: 'error' });
    return;
  }
  if (!isIncrement(price, market.tickSize)) {
    notify({
      message: `Price must be an increment of ${formattedTickSize}`,
      type: 'error',
    });
    return;
  }
  if (price < market.tickSize) {
    notify({ message: 'Price under tick size', type: 'error' });
    return;
  }
  const owner = wallet.publicKey;
  const transaction = new Transaction();
  const signers: Account[] = [];

  if (!baseCurrencyAccount) {
    const {
      transaction: createAccountTransaction,
      newAccountPubkey,
    } = await createTokenAccountTransaction({
      connection,
      wallet,
      mintPublicKey: market.baseMintAddress,
    });
    transaction.add(createAccountTransaction);
    baseCurrencyAccount = newAccountPubkey;
  }
  if (!quoteCurrencyAccount) {
    const {
      transaction: createAccountTransaction,
      newAccountPubkey,
    } = await createTokenAccountTransaction({
      connection,
      wallet,
      mintPublicKey: market.quoteMintAddress,
    });
    transaction.add(createAccountTransaction);
    quoteCurrencyAccount = newAccountPubkey;
  }

  const payer = side === 'sell' ? baseCurrencyAccount : quoteCurrencyAccount;
  if (!payer) {
    notify({
      message: 'Need an SPL token account for cost currency',
      type: 'error',
    });
    return;
  }
  const params = {
    owner,
    payer,
    side,
    price,
    size,
    orderType,
    feeDiscountPubkey: feeDiscountPubkey || null,
  };
  console.log(params);

  const matchOrderstransaction = market.makeMatchOrdersTransaction(5);
  transaction.add(matchOrderstransaction);
  const startTime = getUnixTs();
  let {
    transaction: placeOrderTx,
    signers: placeOrderSigners,
  } = await market.makePlaceOrderTransaction(
    connection,
    params,
    120_000,
    120_000,
  );
  const endTime = getUnixTs();
  console.log(`Creating order transaction took ${endTime - startTime}`);
  transaction.add(placeOrderTx);
  transaction.add(market.makeMatchOrdersTransaction(5));
  signers.push(...placeOrderSigners);

  return await sendTransaction({
    transaction,
    wallet,
    connection,
    signers,
    sendingMessage: 'Sending order...',
  });
}


export async function listMarket({
  connection,
  wallet,
  baseMint,
  quoteMint,
  baseLotSize,
  quoteLotSize,
  dexProgramId,
}: {
  connection: Connection;
  wallet: WalletAdapter;
  baseMint: PublicKey;
  quoteMint: PublicKey;
  baseLotSize: number;
  quoteLotSize: number;
  dexProgramId: PublicKey;
}) {
  const market = new Account();
  const requestQueue = new Account();
  const eventQueue = new Account();
  const bids = new Account();
  const asks = new Account();
  const baseVault = new Account();
  const quoteVault = new Account();
  const feeRateBps = 0;
  const quoteDustThreshold = new BN(100);

  async function getVaultOwnerAndNonce() {
    const nonce = new BN(0);
    while (true) {
      try {
        const vaultOwner = await PublicKey.createProgramAddress(
          [market.publicKey.toBuffer(), nonce.toArrayLike(Buffer, 'le', 8)],
          dexProgramId,
        );
        return [vaultOwner, nonce];
      } catch (e) {
        nonce.iaddn(1);
      }
    }
  }
  const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce();

  const tx1 = new Transaction();
  tx1.add(
    SystemProgram.createAccount({
      fromPubkey: wallet.publicKey,
      newAccountPubkey: baseVault.publicKey,
      lamports: await connection.getMinimumBalanceForRentExemption(165),
      space: 165,
      programId: TokenInstructions.TOKEN_PROGRAM_ID,
    }),
    SystemProgram.createAccount({
      fromPubkey: wallet.publicKey,
      newAccountPubkey: quoteVault.publicKey,
      lamports: await connection.getMinimumBalanceForRentExemption(165),
      space: 165,
      programId: TokenInstructions.TOKEN_PROGRAM_ID,
    }),
    TokenInstructions.initializeAccount({
      account: baseVault.publicKey,
      mint: baseMint,
      owner: vaultOwner,
    }),
    TokenInstructions.initializeAccount({
      account: quoteVault.publicKey,
      mint: quoteMint,
      owner: vaultOwner,
    }),
  );

  const tx2 = new Transaction();
  tx2.add(
    SystemProgram.createAccount({
      fromPubkey: wallet.publicKey,
      newAccountPubkey: market.publicKey,
      lamports: await connection.getMinimumBalanceForRentExemption(
        Market.getLayout(dexProgramId).span,
      ),
      space: Market.getLayout(dexProgramId).span,
      programId: dexProgramId,
    }),
    SystemProgram.createAccount({
      fromPubkey: wallet.publicKey,
      newAccountPubkey: requestQueue.publicKey,
      lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
      space: 5120 + 12,
      programId: dexProgramId,
    }),
    SystemProgram.createAccount({
      fromPubkey: wallet.publicKey,
      newAccountPubkey: eventQueue.publicKey,
      lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
      space: 262144 + 12,
      programId: dexProgramId,
    }),
    SystemProgram.createAccount({
      fromPubkey: wallet.publicKey,
      newAccountPubkey: bids.publicKey,
      lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
      space: 65536 + 12,
      programId: dexProgramId,
    }),
    SystemProgram.createAccount({
      fromPubkey: wallet.publicKey,
      newAccountPubkey: asks.publicKey,
      lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
      space: 65536 + 12,
      programId: dexProgramId,
    }),
    DexInstructions.initializeMarket({
      market: market.publicKey,
      requestQueue: requestQueue.publicKey,
      eventQueue: eventQueue.publicKey,
      bids: bids.publicKey,
      asks: asks.publicKey,
      baseVault: baseVault.publicKey,
      quoteVault: quoteVault.publicKey,
      baseMint,
      quoteMint,
      baseLotSize: new BN(baseLotSize),
      quoteLotSize: new BN(quoteLotSize),
      feeRateBps,
      vaultSignerNonce,
      quoteDustThreshold,
      programId: dexProgramId,
    }),
  );

  const signedTransactions = await signTransactions({
    transactionsAndSigners: [
      { transaction: tx1, signers: [baseVault, quoteVault] },
      {
        transaction: tx2,
        signers: [market, requestQueue, eventQueue, bids, asks],
      },
    ],
    wallet,
    connection,
  });
  for (let signedTransaction of signedTransactions) {
    await sendSignedTransaction({
      signedTransaction,
      connection,
    });
  }

  return market.publicKey;
}


async function getVaultOwnerAndNonce() {
    const nonce = new BN(0);
    while (true) {
      try {
        const vaultOwner = await PublicKey.createProgramAddress(
          [market.publicKey.toBuffer(), nonce.toArrayLike(Buffer, 'le', 8)],
          dexProgramId,
        );
        return [vaultOwner, nonce];
      } catch (e) {
        nonce.iaddn(1);
      }
    }
  }


export async function sendTransaction({
  transaction,
  wallet,
  signers = [],
  connection,
  sendingMessage = 'Sending transaction...',
  sentMessage = 'Transaction sent',
  successMessage = 'Transaction confirmed',
  timeout = DEFAULT_TIMEOUT,
  sendNotification = true,
}: {
  transaction: Transaction;
  wallet: WalletAdapter;
  signers?: Array<Account>;
  connection: Connection;
  sendingMessage?: string;
  sentMessage?: string;
  successMessage?: string;
  timeout?: number;
  sendNotification?: boolean;
}) {
  const signedTransaction = await signTransaction({
    transaction,
    wallet,
    signers,
    connection,
  });
  return await sendSignedTransaction({
    signedTransaction,
    connection,
    sendingMessage,
    sentMessage,
    successMessage,
    timeout,
    sendNotification,
  });
}


export async function signTransaction({
  transaction,
  wallet,
  signers = [],
  connection,
}: {
  transaction: Transaction;
  wallet: WalletAdapter;
  signers?: Array<Account>;
  connection: Connection;
}) {
  transaction.recentBlockhash = (
    await connection.getRecentBlockhash('max')
  ).blockhash;
  transaction.setSigners(wallet.publicKey, ...signers.map((s) => s.publicKey));
  if (signers.length > 0) {
    transaction.partialSign(...signers);
  }
  return await wallet.signTransaction(transaction);
}


export async function signTransactions({
  transactionsAndSigners,
  wallet,
  connection,
}: {
  transactionsAndSigners: {
    transaction: Transaction;
    signers?: Array<Account>;
  }[];
  wallet: WalletAdapter;
  connection: Connection;
}) {
  const blockhash = (await connection.getRecentBlockhash('max')).blockhash;
  transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
    transaction.recentBlockhash = blockhash;
    transaction.setSigners(
      wallet.publicKey,
      ...signers.map((s) => s.publicKey),
    );
    if (signers?.length > 0) {
      transaction.partialSign(...signers);
    }
  });
  return await wallet.signAllTransactions(
    transactionsAndSigners.map(({ transaction }) => transaction),
  );
}


export async function sendSignedTransaction({
  signedTransaction,
  connection,
  sendingMessage = 'Sending transaction...',
  sentMessage = 'Transaction sent',
  successMessage = 'Transaction confirmed',
  timeout = DEFAULT_TIMEOUT,
  sendNotification = true,
}: {
  signedTransaction: Transaction;
  connection: Connection;
  sendingMessage?: string;
  sentMessage?: string;
  successMessage?: string;
  timeout?: number;
  sendNotification?: boolean;
}): Promise<string> {
  const rawTransaction = signedTransaction.serialize();
  const startTime = getUnixTs();
  if (sendNotification) {
    notify({ message: sendingMessage });
  }
  const txid: TransactionSignature = await connection.sendRawTransaction(
    rawTransaction,
    {
      skipPreflight: true,
    },
  );
  if (sendNotification) {
    notify({ message: sentMessage, type: 'success', txid });
  }

  console.log('Started awaiting confirmation for', txid);

  let done = false;
  (async () => {
    while (!done && getUnixTs() - startTime < timeout) {
      connection.sendRawTransaction(rawTransaction, {
        skipPreflight: true,
      });
      await sleep(300);
    }
  })();
  try {
    await awaitTransactionSignatureConfirmation(txid, timeout, connection);
  } catch (err) {
    if (err.timeout) {
      throw new Error('Timed out awaiting confirmation on transaction');
    }
    let simulateResult: SimulatedTransactionResponse | null = null;
    try {
      simulateResult = (
        await simulateTransaction(connection, signedTransaction, 'single')
      ).value;
    } catch (e) {}
    if (simulateResult && simulateResult.err) {
      if (simulateResult.logs) {
        for (let i = simulateResult.logs.length - 1; i >= 0; --i) {
          const line = simulateResult.logs[i];
          if (line.startsWith('Program log: ')) {
            throw new Error(
              'Transaction failed: ' + line.slice('Program log: '.length),
            );
          }
        }
      }
      let parsedError;
      if (
        typeof simulateResult.err == 'object' &&
        'InstructionError' in simulateResult.err
      ) {
        const parsedErrorInfo = parseInstructionErrorResponse(
          signedTransaction,
          simulateResult.err['InstructionError'],
        );
        parsedError = parsedErrorInfo.error;
      } else {
        parsedError = JSON.stringify(simulateResult.err);
      }
      throw new Error(parsedError);
    }
    throw new Error('Transaction failed');
  } finally {
    done = true;
  }
  if (sendNotification) {
    notify({ message: successMessage, type: 'success', txid });
  }

  console.log('Latency', txid, getUnixTs() - startTime);
  return txid;
}


async function awaitTransactionSignatureConfirmation(
  txid: TransactionSignature,
  timeout: number,
  connection: Connection,
) {
  let done = false;
  const result = await new Promise((resolve, reject) => {
    (async () => {
      setTimeout(() => {
        if (done) {
          return;
        }
        done = true;
        console.log('Timed out for txid', txid);
        reject({ timeout: true });
      }, timeout);
      try {
        connection.onSignature(
          txid,
          (result) => {
            console.log('WS confirmed', txid, result);
            done = true;
            if (result.err) {
              reject(result.err);
            } else {
              resolve(result);
            }
          },
          'recent',
        );
        console.log('Set up WS connection', txid);
      } catch (e) {
        done = true;
        console.log('WS error in setup', txid, e);
      }
      while (!done) {
        // eslint-disable-next-line no-loop-func
        (async () => {
          try {
            const signatureStatuses = await connection.getSignatureStatuses([
              txid,
            ]);
            const result = signatureStatuses && signatureStatuses.value[0];
            if (!done) {
              if (!result) {
                console.log('REST null result for', txid, result);
              } else if (result.err) {
                console.log('REST error for', txid, result);
                done = true;
                reject(result.err);
              } else if (!result.confirmations) {
                console.log('REST no confirmations for', txid, result);
              } else {
                console.log('REST confirmation for', txid, result);
                done = true;
                resolve(result);
              }
            }
          } catch (e) {
            if (!done) {
              console.log('REST connection error: txid', txid, e);
            }
          }
        })();
        await sleep(300);
      }
    })();
  });
  done = true;
  return result;
}


function mergeTransactions(transactions: (Transaction | undefined)[]) {
  const transaction = new Transaction();
  transactions
    .filter((t): t is Transaction => t !== undefined)
    .forEach((t) => {
      transaction.add(t);
    });
  return transaction;
}