// Copyright (C) 2023 The Android Open Source Project
//
// 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 {Registry} from '../base/registry';
import type {
  TrackRenderer,
  Track,
  TrackManager,
  TrackFilterCriteria,
  Overlay,
} from '../public/track';
import type {TrackRenderContext} from '../public/track';
import type {TrackNode} from '../public/workspace';
import type {TraceImpl} from './trace_impl';

export interface TrackWrapper {
  readonly track: TrackRenderer;
  readonly desc: Track;
  render(ctx: TrackRenderContext): void;
  getError(): Error | undefined;
}

export class TrackFilterState {
  public nameFilter: string = '';
  public criteriaFilters = new Map<string, string[]>();

  // Clear all filters.
  clearAll() {
    this.nameFilter = '';
    this.criteriaFilters.clear();
  }

  // Returns true if any filters are set.
  areFiltersSet() {
    return this.nameFilter !== '' || this.criteriaFilters.size > 0;
  }
}

/**
 * TrackManager is responsible for managing the registry of tracks.
 * Tracks are registered via registerTrack() and looked up via getWrappedTrack().
 * Each track is wrapped in a TrackWrapperImpl which handles error containment.
 */
export class TrackManagerImpl implements TrackManager {
  private readonly tracks = new Registry<TrackWrapperImpl>((x) => x.desc.uri);
  private readonly _overlays: Overlay[] = [];

  // This property is written by scroll_helper.ts and read&cleared by the
  // track_panel.ts. This exist for the following use case: the user wants to
  // scroll to track X, but X is not visible because it's in a collapsed group.
  // So we want to stash this information in a place that track_panel.ts can
  // access when creating dom elements.
  //
  // Note: this is the node id of the track node to scroll to, not the track
  // uri, as this allows us to scroll to tracks that have no uri.
  scrollToTrackNodeId?: string;

  // List of registered filter criteria.
  readonly filterCriteria: TrackFilterCriteria[] = [];

  // Current state of the track filters.
  readonly filters = new TrackFilterState();

  registerTrack(trackDesc: Track): Disposable {
    return this.tracks.register(new TrackWrapperImpl(trackDesc));
  }

  registerOverlay(overlay: Overlay): Disposable {
    this._overlays.push(overlay);
    return {
      [Symbol.dispose]: () => {
        const index = this._overlays.indexOf(overlay);
        if (index !== -1) {
          this._overlays.splice(index, 1);
        }
      },
    };
  }

  findTrack(
    predicate: (desc: Track) => boolean | undefined,
  ): Track | undefined {
    for (const t of this.tracks.values()) {
      if (predicate(t.desc)) return t.desc;
    }
    return undefined;
  }

  getAllTracks(): Track[] {
    return Array.from(this.tracks.valuesAsArray().map((t) => t.desc));
  }

  // Look up track into for a given track's URI.
  // Returns |undefined| if no track can be found.
  getTrack(uri: string): Track | undefined {
    return this.tracks.tryGet(uri)?.desc;
  }

  // Returns a wrapped track that provides error containment for rendering.
  getWrappedTrack(uri: string): TrackWrapper | undefined {
    return this.tracks.tryGet(uri);
  }

  registerTrackFilterCriteria(filter: TrackFilterCriteria): void {
    this.filterCriteria.push(filter);
  }

  get trackFilterCriteria(): ReadonlyArray<TrackFilterCriteria> {
    return this.filterCriteria;
  }

  get overlays(): ReadonlyArray<Overlay> {
    return this._overlays;
  }
}

/**
 * Wraps a track and provides error containment. If render() throws, the error
 * is captured and subsequent render() calls become no-ops. This prevents a
 * crashing track from breaking the entire UI.
 */
class TrackWrapperImpl implements TrackWrapper {
  public readonly desc: Track;
  private error?: Error;

  constructor(desc: Track) {
    this.desc = desc;
  }

  render(ctx: TrackRenderContext): void {
    // Don't enter the track again once an error is has occurred
    if (this.error) {
      return;
    }

    try {
      this.track.render(ctx);
    } catch (e) {
      this.error = e;
    }
  }

  getError(): Error | undefined {
    return this.error;
  }

  get track(): TrackRenderer {
    return this.desc.renderer;
  }
}

// Returns true if a track matches the configured track filters.
export function trackMatchesFilter(
  trace: TraceImpl,
  track: TrackNode,
): boolean {
  const filters = trace.tracks.filters;

  // Check the name filter.
  if (filters.nameFilter !== '') {
    // Split terms on commas and remove the whitespace.
    const nameFilters = filters.nameFilter
      .split(',')
      .map((s) => s.trim())
      .filter((s) => s !== '');

    // At least one of the name filter terms must match.
    const trackTitleLower = track.name.toLowerCase();
    if (
      !nameFilters.some((nameFilter) =>
        trackTitleLower.includes(nameFilter.toLowerCase()),
      )
    ) {
      return false;
    }
  }

  // Check all the criteria filters.
  for (const [criteriaName, values] of filters.criteriaFilters) {
    const criteriaFilter = trace.tracks.trackFilterCriteria.find(
      (c) => c.name === criteriaName,
    );

    if (!criteriaFilter) {
      continue;
    }

    // At least one of the criteria filters must match.
    if (!values.some((value) => criteriaFilter.predicate(track, value))) {
      return false;
    }
  }

  return true;
}
