/*
 * Copyright (C) 2022 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 {
  ChangeDetectorRef,
  Component,
  ElementRef,
  EventEmitter,
  HostListener,
  Inject,
  Input,
  Output,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {TimelineData} from 'app/timeline_data';
import {TRACE_INFO} from 'app/trace_info';
import {assertDefined} from 'common/assert_utils';
import {FunctionUtils} from 'common/function_utils';
import {StringUtils} from 'common/string_utils';
import {TimeUtils} from 'common/time_utils';
import {
  OnTracePositionUpdate,
  TracePositionUpdateEmitter,
} from 'interfaces/trace_position_update_emitter';
import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener';
import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {MiniTimelineComponent} from './mini_timeline_component';
@Component({
  selector: 'timeline',
  encapsulation: ViewEncapsulation.None,
  template: `
    
      
        
          No screenrecording frame to show
          Current timestamp before first screenrecording frame.
         
       
       
    
      
        
          
            chevron_left 
           
          
          
            chevron_right 
           
        
        
          
            
              Select up to 2 additional traces to display.
              
                {{ TRACE_INFO[trace].icon }} 
                {{ TRACE_INFO[trace].name }}
               
              
                Cancel 
                
                  Apply
                 
              
              
                
                  {{ TRACE_INFO[selectedTrace].icon }}
                 
               
             
           
         
        
          
            expand_less 
            expand_more 
           
        
       
      
        No timeline to show!
        All loaded traces contain no timestamps!
       
      
        No timeline to show!
        Only a single timestamp has been recorded.
       
     
  `,
  styles: [
    `
      .navbar {
        display: flex;
        width: 100%;
        flex-direction: row;
        align-items: center;
        justify-content: center;
      }
      #expanded-nav {
        display: flex;
        border-bottom: 1px solid #3333;
      }
      #time-selector {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: center;
      }
      .time-selector-form {
        display: flex;
        flex-direction: column;
        width: 15em;
      }
      .time-selector-form .time-input {
        width: 100%;
        margin-bottom: -1.34375em;
        text-align: center;
      }
      #mini-timeline {
        flex-grow: 1;
        align-self: stretch;
      }
      #video-content {
        position: relative;
        min-width: 20rem;
        min-height: 35rem;
        align-self: stretch;
        text-align: center;
        border: 2px solid black;
        flex-basis: 0px;
        flex-grow: 1;
        display: flex;
        align-items: center;
      }
      #video {
        position: absolute;
        left: 0;
        top: 0;
        height: 100%;
        width: 100%;
      }
      #expanded-nav {
        display: flex;
        flex-direction: row;
      }
      #expanded-timeline {
        flex-grow: 1;
      }
      #trace-selector .mat-form-field-infix {
        width: 50px;
        padding: 0 0.75rem 0 0.5rem;
        border-top: unset;
      }
      #trace-selector .mat-icon {
        padding: 2px;
      }
      #trace-selector .shown-selection {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        height: auto;
      }
      #trace-selector .mat-select-trigger {
        height: unset;
      }
      #trace-selector .mat-form-field-wrapper {
        padding: 0;
      }
      .mat-select-panel {
        max-height: unset !important;
        font-family: 'Roboto', sans-serif;
      }
      .tip {
        padding: 1.5rem;
        font-weight: 200;
        border-bottom: solid 1px #dadce0;
      }
      .actions {
        border-top: solid 1px #dadce0;
        width: 100%;
        padding: 1.5rem;
        float: right;
        display: flex;
        justify-content: flex-end;
      }
      .no-video-message {
        padding: 1rem;
        font-family: 'Roboto', sans-serif;
      }
      .no-timestamps-msg {
        padding: 1rem;
        align-items: center;
        display: flex;
        flex-direction: column;
      }
    `,
  ],
})
export class TimelineComponent implements TracePositionUpdateEmitter, TracePositionUpdateListener {
  readonly TOGGLE_BUTTON_CLASS: string = 'button-toggle-expansion';
  readonly MAX_SELECTED_TRACES = 3;
  @Input() set activeViewTraceTypes(types: TraceType[] | undefined) {
    if (!types) {
      return;
    }
    if (types.length !== 1) {
      throw Error("Timeline component doesn't support viewers with dependencies length !== 1");
    }
    this.internalActiveTrace = types[0];
    if (!this.selectedTraces.includes(this.internalActiveTrace)) {
      this.selectedTraces.push(this.internalActiveTrace);
    }
    if (this.selectedTraces.length > this.MAX_SELECTED_TRACES) {
      // Maxed capacity so remove oldest selected trace
      this.selectedTraces = this.selectedTraces.slice(1, 1 + this.MAX_SELECTED_TRACES);
    }
    // Create new object to make sure we trigger an update on Mini Timeline child component
    this.selectedTraces = [...this.selectedTraces];
    this.selectedTracesFormControl.setValue(this.selectedTraces);
  }
  internalActiveTrace: TraceType | undefined = undefined;
  @Input() timelineData!: TimelineData;
  @Input() availableTraces: TraceType[] = [];
  @Output() collapsedTimelineSizeChanged = new EventEmitter();
  @ViewChild('miniTimeline') private miniTimelineComponent!: MiniTimelineComponent;
  @ViewChild('collapsedTimeline') private collapsedTimelineRef!: ElementRef;
  selectedTraces: TraceType[] = [];
  selectedTracesFormControl = new FormControl();
  selectedElapsedTimeFormControl = new FormControl(
    'undefined',
    Validators.compose([
      Validators.required,
      Validators.pattern(TimeUtils.HUMAN_ELAPSED_TIMESTAMP_REGEX),
    ])
  );
  selectedRealTimeFormControl = new FormControl(
    'undefined',
    Validators.compose([
      Validators.required,
      Validators.pattern(TimeUtils.HUMAN_REAL_TIMESTAMP_REGEX),
    ])
  );
  selectedNsFormControl = new FormControl(
    'undefined',
    Validators.compose([Validators.required, Validators.pattern(TimeUtils.NS_TIMESTAMP_REGEX)])
  );
  timestampForm = new FormGroup({
    selectedElapsedTime: this.selectedElapsedTimeFormControl,
    selectedRealTime: this.selectedRealTimeFormControl,
    selectedNs: this.selectedNsFormControl,
  });
  videoUrl: SafeUrl | undefined;
  private expanded = false;
  TRACE_INFO = TRACE_INFO;
  private onTracePositionUpdateCallback: OnTracePositionUpdate = FunctionUtils.DO_NOTHING_ASYNC;
  constructor(
    @Inject(DomSanitizer) private sanitizer: DomSanitizer,
    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef
  ) {}
  ngOnInit() {
    if (this.timelineData.hasTimestamps()) {
      this.updateTimeInputValuesToCurrentTimestamp();
    }
    const screenRecordingVideo = this.timelineData.getScreenRecordingVideo();
    if (screenRecordingVideo) {
      this.videoUrl = this.sanitizer.bypassSecurityTrustUrl(
        URL.createObjectURL(screenRecordingVideo)
      );
    }
  }
  ngAfterViewInit() {
    const height = this.collapsedTimelineRef.nativeElement.offsetHeight;
    this.collapsedTimelineSizeChanged.emit(height);
  }
  setOnTracePositionUpdate(callback: OnTracePositionUpdate) {
    this.onTracePositionUpdateCallback = callback;
  }
  getVideoCurrentTime() {
    return this.timelineData.searchCorrespondingScreenRecordingTimeSeconds(
      this.getCurrentTracePosition()
    );
  }
  private seekTracePosition?: TracePosition;
  getCurrentTracePosition(): TracePosition {
    if (this.seekTracePosition) {
      return this.seekTracePosition;
    }
    const position = this.timelineData.getCurrentPosition();
    if (position === undefined) {
      throw Error('A trace position should be available by the time the timeline is loaded');
    }
    return position;
  }
  onTracePositionUpdate(position: TracePosition) {
    this.updateTimeInputValuesToCurrentTimestamp();
  }
  toggleExpand() {
    this.expanded = !this.expanded;
    this.changeDetectorRef.detectChanges();
  }
  async updatePosition(position: TracePosition) {
    this.timelineData.setPosition(position);
    await this.onTracePositionUpdateCallback(position);
  }
  usingRealtime(): boolean {
    return this.timelineData.getTimestampType() === TimestampType.REAL;
  }
  updateSeekTimestamp(timestamp: Timestamp | undefined) {
    if (timestamp) {
      this.seekTracePosition = TracePosition.fromTimestamp(timestamp);
    } else {
      this.seekTracePosition = undefined;
    }
    this.updateTimeInputValuesToCurrentTimestamp();
  }
  private updateTimeInputValuesToCurrentTimestamp() {
    this.selectedElapsedTimeFormControl.setValue(
      TimeUtils.format(
        new ElapsedTimestamp(this.getCurrentTracePosition().timestamp.getValueNs()),
        false
      )
    );
    this.selectedRealTimeFormControl.setValue(
      TimeUtils.format(new RealTimestamp(this.getCurrentTracePosition().timestamp.getValueNs()))
    );
    this.selectedNsFormControl.setValue(
      `${this.getCurrentTracePosition().timestamp.getValueNs()} ns`
    );
  }
  isOptionDisabled(trace: TraceType) {
    if (this.internalActiveTrace === trace) {
      return true;
    }
    // Reached limit of options and is not a selected element
    if (
      (this.selectedTracesFormControl.value?.length ?? 0) >= this.MAX_SELECTED_TRACES &&
      this.selectedTracesFormControl.value?.find((el: TraceType) => el === trace) === undefined
    ) {
      return true;
    }
    return false;
  }
  onTraceSelectionClosed() {
    this.selectedTracesFormControl.setValue(this.selectedTraces);
  }
  applyNewTraceSelection() {
    this.selectedTraces = this.selectedTracesFormControl.value;
  }
  @HostListener('document:keydown', ['$event'])
  async handleKeyboardEvent(event: KeyboardEvent) {
    if (event.key === 'ArrowLeft') {
      await this.moveToPreviousEntry();
    } else if (event.key === 'ArrowRight') {
      await this.moveToNextEntry();
    }
  }
  hasPrevEntry(): boolean {
    if (!this.internalActiveTrace) {
      return false;
    }
    if (this.timelineData.getTraces().getTrace(this.internalActiveTrace) === undefined) {
      return false;
    }
    return this.timelineData.getPreviousEntryFor(this.internalActiveTrace) !== undefined;
  }
  hasNextEntry(): boolean {
    if (!this.internalActiveTrace) {
      return false;
    }
    if (this.timelineData.getTraces().getTrace(this.internalActiveTrace) === undefined) {
      return false;
    }
    return this.timelineData.getNextEntryFor(this.internalActiveTrace) !== undefined;
  }
  async moveToPreviousEntry() {
    if (!this.internalActiveTrace) {
      return;
    }
    this.timelineData.moveToPreviousEntryFor(this.internalActiveTrace);
    await this.onTracePositionUpdateCallback(assertDefined(this.timelineData.getCurrentPosition()));
  }
  async moveToNextEntry() {
    if (!this.internalActiveTrace) {
      return;
    }
    this.timelineData.moveToNextEntryFor(this.internalActiveTrace);
    await this.onTracePositionUpdateCallback(assertDefined(this.timelineData.getCurrentPosition()));
  }
  async humanElapsedTimeInputChange(event: Event) {
    if (event.type !== 'change') {
      return;
    }
    const target = event.target as HTMLInputElement;
    const timestamp = TimeUtils.parseHumanElapsed(target.value);
    await this.updatePosition(TracePosition.fromTimestamp(timestamp));
    this.updateTimeInputValuesToCurrentTimestamp();
  }
  async humanRealTimeInputChanged(event: Event) {
    if (event.type !== 'change') {
      return;
    }
    const target = event.target as HTMLInputElement;
    const timestamp = TimeUtils.parseHumanReal(target.value);
    await this.updatePosition(TracePosition.fromTimestamp(timestamp));
    this.updateTimeInputValuesToCurrentTimestamp();
  }
  async nanosecondsInputTimeChange(event: Event) {
    if (event.type !== 'change') {
      return;
    }
    const target = event.target as HTMLInputElement;
    const timestamp = new Timestamp(
      this.timelineData.getTimestampType()!,
      StringUtils.parseBigIntStrippingUnit(target.value)
    );
    await this.updatePosition(TracePosition.fromTimestamp(timestamp));
    this.updateTimeInputValuesToCurrentTimestamp();
  }
}