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

import { parseClientSideCallMetadata } from '@isomorphic/traceUtils';

import { SnapshotStorage } from './snapshotStorage';
import { TraceModernizer } from './traceModernizer';

import type { ContextEntry } from '../types/entries';

export interface TraceModelBackend {
  entryNames(): Promise<string[]>;
  hasEntry(entryName: string): Promise<boolean>;
  readText(entryName: string): Promise<string | undefined>;
  readBlob(entryName: string): Promise<Blob | undefined>;
  isLive(): boolean;
  traceURL(): string;
}

export class TraceModel {
  contextEntries: ContextEntry[] = [];
  private _snapshotStorage: SnapshotStorage | undefined;
  private _backend!: TraceModelBackend;
  private _resourceToContentType = new Map<string, string>();

  constructor() {
  }

  async load(backend: TraceModelBackend, unzipProgress: (done: number, total: number) => void) {
    this._backend = backend;

    const ordinals: string[] = [];
    let hasSource = false;
    for (const entryName of await this._backend.entryNames()) {
      const match = entryName.match(/(.+)\.trace$/);
      if (match)
        ordinals.push(match[1] || '');
      if (entryName.includes('src@'))
        hasSource = true;
    }
    if (!ordinals.length)
      throw new Error('Cannot find .trace file');

    this._snapshotStorage = new SnapshotStorage();

    // 3 * ordinals progress increments below.
    const total = ordinals.length * 3;
    let done = 0;
    for (const ordinal of ordinals) {
      const contextEntry = createEmptyContext();
      contextEntry.hasSource = hasSource;
      const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage);

      const trace = await this._backend.readText(ordinal + '.trace') || '';
      modernizer.appendTrace(trace);
      unzipProgress(++done, total);

      const network = await this._backend.readText(ordinal + '.network') || '';
      modernizer.appendTrace(network);
      unzipProgress(++done, total);

      contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);

      if (!backend.isLive()) {
        // Terminate actions w/o after event gracefully.
        // This would close after hooks event that has not been closed because
        // the trace is usually saved before after hooks complete.
        for (const action of contextEntry.actions.slice().reverse()) {
          if (!action.endTime && !action.error) {
            for (const a of contextEntry.actions) {
              if (a.parentId === action.callId && action.endTime < a.endTime)
                action.endTime = a.endTime;
            }
          }
        }
      }

      const stacks = await this._backend.readText(ordinal + '.stacks');
      if (stacks) {
        const callMetadata = parseClientSideCallMetadata(JSON.parse(stacks));
        for (const action of contextEntry.actions)
          action.stack = action.stack || callMetadata.get(action.callId);
      }
      unzipProgress(++done, total);

      for (const resource of contextEntry.resources) {
        if (resource.request.postData?._sha1)
          this._resourceToContentType.set(resource.request.postData._sha1, stripEncodingFromContentType(resource.request.postData.mimeType));
        if (resource.response.content?._sha1)
          this._resourceToContentType.set(resource.response.content._sha1, stripEncodingFromContentType(resource.response.content.mimeType));
      }

      this.contextEntries.push(contextEntry);
    }

    this._snapshotStorage.finalize();
  }

  async hasEntry(filename: string): Promise<boolean> {
    return this._backend.hasEntry(filename);
  }

  async resourceForSha1(sha1: string): Promise<Blob | undefined> {
    const blob = await this._backend.readBlob('resources/' + sha1);
    const contentType = this._resourceToContentType.get(sha1);
    // "x-unknown" in the har means "no content type".
    if (!blob || contentType === undefined || contentType === 'x-unknown')
      return blob;
    return new Blob([blob], { type: contentType });
  }

  storage(): SnapshotStorage {
    return this._snapshotStorage!;
  }
}

function stripEncodingFromContentType(contentType: string) {
  const charset = contentType.match(/^(.*);\s*charset=.*$/);
  if (charset)
    return charset[1];
  return contentType;
}

function createEmptyContext(): ContextEntry {
  return {
    origin: 'testRunner',
    startTime: Number.MAX_SAFE_INTEGER,
    wallTime: Number.MAX_SAFE_INTEGER,
    endTime: 0,
    browserName: '',
    options: {
      deviceScaleFactor: 1,
      isMobile: false,
      viewport: { width: 1280, height: 800 },
    },
    pages: [],
    resources: [],
    actions: [],
    events: [],
    errors: [],
    stdio: [],
    hasSource: false,
    contextId: '',
  };
}
