<!-- Copyright (C) 2020 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.
-->
<template>
  <div class="overlay" v-if="hasTimeline || video">
    <div class="overlay-content" ref="overlayContent">
      <draggable-div
        ref="videoOverlay"
        class="video-overlay"
        v-show="minimized && showVideoOverlay"
        position="bottomLeft"
        :asyncLoad="true"
        :resizeable="true"
        v-on:requestExtraWidth="updateVideoOverlayWidth"
        :style="videoOverlayStyle"
        v-if="video"
      >
        <template slot="header">
          <div class="close-video-overlay" @click="closeVideoOverlay">
            <md-icon>
              close
              <md-tooltip md-direction="right">Close video overlay</md-tooltip>
            </md-icon>
          </div>
        </template>
        <template slot="main">
          <div ref="overlayVideoContainer">
            <videoview
              ref="video"
              :file="video"
              :height="videoHeight"
              @loaded="videoLoaded" />
          </div>
        </template>
      </draggable-div>
    </div>
    <md-bottom-bar
      class="bottom-nav"
      v-if="hasTimeline || (video && !showVideoOverlay)"
      ref="bottomNav"
    >
      <div class="nav-content">
        <div class="">
          <searchbar
            class="search-bar"
            v-if="search"
            :searchTypes="searchTypes"
            :store="store"
            :presentTags="Object.freeze(presentTags)"
            :presentErrors="Object.freeze(presentErrors)"
            :timeline="mergedTimeline.timeline"
          />
          <md-toolbar
            md-elevation="0"
            class="md-transparent">

            <md-button
              @click="toggleSearch()"
              class="drop-search"
            >
              Toggle search bar
            </md-button>

            <div class="toolbar" :class="{ expanded: expanded }">
              <div class="resize-bar" v-show="expanded">
                <div v-if="video" @mousedown="resizeBottomNav">
                  <md-icon class="drag-handle">
                    drag_handle
                    <md-tooltip md-direction="top">resize</md-tooltip>
                  </md-icon>
                </div>
              </div>

              <div class="active-timeline" v-show="minimized">
                <div
                  class="active-timeline-icon"
                  @click="$refs.navigationTypeSelection.$el
                          .querySelector('input').click()"
                >
                  <md-icon class="collapsed-timeline-icon">
                    {{ collapsedTimelineIcon }}
                    <md-tooltip>
                      {{ collapsedTimelineIconTooltip }}
                    </md-tooltip>
                  </md-icon>
                </div>

                <md-field
                  v-if="multipleTraces"
                  ref="navigationTypeSelection"
                  class="navigation-style-selection-field"
                >

                  <label>Navigation</label>
                  <md-select
                    v-model="navigationStyle"
                    name="navigationStyle"
                    md-dense
                  >
                    <md-icon-option
                      :value="NAVIGATION_STYLE.GLOBAL"
                      icon="public"
                      desc="Consider all timelines for navigation"
                    />
                    <md-icon-option
                      :value="NAVIGATION_STYLE.FOCUSED"
                      :icon="TRACE_ICONS[focusedFile.type]"
                      :desc="`Automatically switch what timeline is considered
                        for navigation based on what is visible on screen.
                        Currently ${focusedFile.type}.`"
                    />
                    <!-- TODO: Add edit button for custom settings that opens
                               popup dialog menu -->
                    <md-icon-option
                      :value="NAVIGATION_STYLE.CUSTOM"
                      icon="dashboard_customize"
                      desc="Considers only the enabled timelines for
                            navigation. Expand the bottom bar to toggle
                            timelines."
                    />
                    <md-optgroup label="Targeted">
                      <md-icon-option
                        v-for="file in timelineFiles"
                        v-bind:key="file.type"
                        :value="`${NAVIGATION_STYLE.TARGETED}-` +
                                `${file.type}`"
                        :displayValue="file.type"
                        :shortValue="NAVIGATION_STYLE.TARGETED"
                        :icon="TRACE_ICONS[file.type]"
                        :desc="`Only consider ${file.type} ` +
                               'for timeline navigation.'"
                      />
                    </md-optgroup>
                  </md-select>
                </md-field>
              </div>

              <div
                class="minimized-timeline-content"
                v-show="minimized"
                v-if="hasTimeline"
              >
                <input
                  class="timestamp-search-input"
                  v-model="searchInput"
                  spellcheck="false"
                  :placeholder="seekTime"
                  @focus="updateInputMode(true)"
                  @blur="updateInputMode(false)"
                  @keyup.enter="updateSearchForTimestamp"
                />
                <timeline
                  :store="store"
                  :flickerMode="flickerMode"
                  :tags="Object.freeze(presentTags)"
                  :errors="Object.freeze(presentErrors)"
                  :timeline="Object.freeze(minimizedTimeline.timeline)"
                  :selected-index="minimizedTimeline.selectedIndex"
                  :scale="scale"
                  :crop="crop"
                  class="minimized-timeline"
                />
              </div>

              <md-button
                class="md-icon-button show-video-overlay-btn"
                :class="{active: minimized && showVideoOverlay}"
                @click="toggleVideoOverlay"
                v-show="minimized"
                style="margin-bottom: 10px;"
              >
                <i class="md-icon md-icon-font">
                  featured_video
                </i>
                <md-tooltip md-direction="top">
                  <span v-if="showVideoOverlay">Hide video overlay</span>
                  <span v-else>Show video overlay</span>
                </md-tooltip>
              </md-button>

              <md-button
                class="md-icon-button toggle-btn"
                @click="toggle"
                style="margin-bottom: 10px;"
              >
                <md-icon v-if="minimized">
                  expand_less
                  <md-tooltip md-direction="top" @click="buttonClicked(`Expand Timeline`)">Expand timeline</md-tooltip>
                </md-icon>
                <md-icon v-else>
                  expand_more
                  <md-tooltip md-direction="top" @click="buttonClicked(`Collapse Timeline`)">Collapse timeline</md-tooltip>
                </md-icon>
              </md-button>
            </div>
          </md-toolbar>

          <div class="expanded-content" v-show="expanded">
            <div :v-if="video">
              <div
                class="expanded-content-video"
                ref="expandedContentVideoContainer"
              >
                <!-- Video moved here on expansion -->
              </div>
            </div>
            <div class="flex-fill">
              <div
                ref="expandedTimeline"
                :style="`padding-top: ${resizeOffset}px;`"
              >
                <div class="seek-time" v-if="seekTime">
                  <b>Seek time: </b>
                  <input
                    class="timestamp-search-input"
                    :class="{ expanded: expanded }"
                    v-model="searchInput"
                    spellcheck="false"
                    :placeholder="seekTime"
                    @focus="updateInputMode(true)"
                    @blur="updateInputMode(false)"
                    @keyup.enter="updateSearchForTimestamp"
                  />
                </div>

                <timelines
                  :timelineFiles="timelineFiles"
                  :scale="scale"
                  :crop="crop"
                  :cropIntent="cropIntent"
                  v-on:crop="onTimelineCrop"
                />

                <div class="timeline-selection">
                  <div class="timeline-selection-header">
                    <label>Timeline Area Selection</label>
                    <span class="material-icons help-icon">
                      help_outline
                      <md-tooltip md-direction="right">
                        Select the area of the timeline to focus on.
                        Click and drag to select.
                      </md-tooltip>
                    </span>
                    <md-button
                      class="md-primary"
                      v-if="isCropped"
                      @click.native="clearSelection"
                    >
                      Clear selection
                    </md-button>
                  </div>
                  <timeline-selection
                    :timeline="mergedTimeline.timeline"
                    :start-timestamp="0"
                    :end-timestamp="0"
                    :scale="scale"
                    :cropArea="crop"
                    v-on:crop="onTimelineCrop"
                    v-on:cropIntent="onTimelineCropIntent"
                    v-on:showVideoAt="changeVideoTimestamp"
                    v-on:resetVideoTimestamp="resetVideoTimestamp"
                  />
                </div>

                <div class="help" v-if="!minimized">
                  <div class="help-icon-wrapper">
                    <span class="material-icons help-icon">
                      help_outline
                      <md-tooltip md-direction="left">
                        Click on icons to disable timelines
                      </md-tooltip>
                    </span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </md-bottom-bar>
  </div>
</template>
<script>
import Timeline from './Timeline.vue';
import Timelines from './Timelines.vue';
import TimelineSelection from './TimelineSelection.vue';
import DraggableDiv from './DraggableDiv.vue';
import VideoView from './VideoView.vue';
import MdIconOption from './components/IconSelection/IconSelectOption.vue';
import Searchbar from './Searchbar.vue';
import FileType from './mixins/FileType.js';
import {NAVIGATION_STYLE} from './utils/consts';
import {TRACE_ICONS} from '@/decode.js';

// eslint-disable-next-line camelcase
import {nanos_to_string, getClosestTimestamp} from './transform.js';

export default {
  name: 'overlay',
  props: ['store', 'presentTags', 'presentErrors', 'searchTypes'],
  mixins: [FileType],
  data() {
    return {
      minimized: true,
      // height of video in expanded timeline,
      // made to match expandedTimeline dynamically
      videoHeight: 'auto',
      dragState: {
        clientY: null,
        lastDragEndPosition: null,
      },
      resizeOffset: 0,
      showVideoOverlay: true,
      mergedTimeline: null,
      NAVIGATION_STYLE,
      navigationStyle: this.store.navigationStyle,
      videoOverlayExtraWidth: 0,
      crop: null,
      cropIntent: null,
      TRACE_ICONS,
      search: false,
      searchInput: "",
      isSeekTimeInputMode: false,
    };
  },
  created() {
    this.mergedTimeline = this.computeMergedTimeline();
    this.$store.commit('setMergedTimeline', this.mergedTimeline);
    this.updateNavigationFileFilter();
  },
  mounted() {
    this.emitBottomHeightUpdate();
  },
  destroyed() {
    this.$store.commit('removeMergedTimeline', this.mergedTimeline);
    this.updateInputMode(false);
  },
  watch: {
    navigationStyle(style) {
      // Only store navigation type in local store if it's a type that will
      // work regardless of what data is loaded.
      if (style === NAVIGATION_STYLE.GLOBAL ||
        style === NAVIGATION_STYLE.FOCUSED) {
        this.store.navigationStyle = style;
      }
      this.updateNavigationFileFilter();
    },
    minimized() {
      // Minimized toggled
      this.updateNavigationFileFilter();

      this.$nextTick(this.emitBottomHeightUpdate);
    },
  },
  computed: {
    video() {
      return this.$store.getters.video;
    },
    videoOverlayStyle() {
      return {
        width: 150 + this.videoOverlayExtraWidth + 'px',
      };
    },
    timelineFiles() {
      return this.$store.getters.timelineFiles;
    },
    focusedFile() {
      return this.$store.state.focusedFile;
    },
    expanded() {
      return !this.minimized;
    },
    seekTime() {
      return nanos_to_string(this.currentTimestamp);
    },
    scale() {
      const mx = Math.max(...(this.timelineFiles.map((f) =>
        Math.max(...f.timeline))));
      const mi = Math.min(...(this.timelineFiles.map((f) =>
        Math.min(...f.timeline))));
      return [mi, mx];
    },
    currentTimestamp() {
      return this.$store.state.currentTimestamp;
    },
    hasTimeline() {
      // Returns true if a meaningful timeline exists (i.e. not only dumps)
      for (const file of this.timelineFiles) {
        if (file.timeline.length > 0 &&
            (file.timeline[0] !== undefined || file.timeline.length > 1)) {
          return true;
        }
      }

      return false;
    },
    collapsedTimelineIconTooltip() {
      switch (this.navigationStyle) {
        case NAVIGATION_STYLE.GLOBAL:
          return 'All timelines';

        case NAVIGATION_STYLE.FOCUSED:
          return `Focused: ${this.focusedFile.type}`;

        case NAVIGATION_STYLE.CUSTOM:
          return 'Enabled timelines';

        default:
          const split = this.navigationStyle.split('-');
          if (split[0] !== NAVIGATION_STYLE.TARGETED) {
            console.warn('Unexpected navigation type; fallback to global');
            return 'All timelines';
          }

          const fileType = split[1];

          return fileType;
      }
    },
    collapsedTimelineIcon() {
      switch (this.navigationStyle) {
        case NAVIGATION_STYLE.GLOBAL:
          return 'public';

        case NAVIGATION_STYLE.FOCUSED:
          return TRACE_ICONS[this.focusedFile.type];

        case NAVIGATION_STYLE.CUSTOM:
          return 'dashboard_customize';

        default:
          const split = this.navigationStyle.split('-');
          if (split[0] !== NAVIGATION_STYLE.TARGETED) {
            console.warn('Unexpected navigation type; fallback to global');
            return 'public';
          }

          const fileType = split[1];

          return TRACE_ICONS[fileType];
      }
    },
    minimizedTimeline() {
      if (this.navigationStyle === NAVIGATION_STYLE.GLOBAL) {
        return this.mergedTimeline;
      }

      if (this.navigationStyle === NAVIGATION_STYLE.FOCUSED) {
        //dumps do not have a timeline, so if scrolling over a dump, show merged timeline
        if (this.focusedFile.timeline) {
          return this.focusedFile;
        }
        return this.mergedTimeline;
      }

      if (this.navigationStyle === NAVIGATION_STYLE.CUSTOM) {
        // TODO: Return custom timeline
        return this.mergedTimeline;
      }

      if (
        this.navigationStyle.split('-').length >= 2
        && this.navigationStyle.split('-')[0] === NAVIGATION_STYLE.TARGETED
      ) {
        return this.$store.state
            .traces[this.navigationStyle.split('-')[1]];
      }

      console.warn('Unexpected navigation type; fallback to global');
      return this.mergedTimeline;
    },
    isCropped() {
      return this.crop != null &&
        (this.crop.left !== 0 || this.crop.right !== 1);
    },
    multipleTraces() {
      return this.timelineFiles.length > 1;
    },
    flickerMode() {
      return this.presentTags.length>0 || this.presentErrors.length>0;
    },
  },
  updated() {
    this.$nextTick(() => {
      if (this.$refs.expandedTimeline && this.expanded) {
        this.videoHeight = this.$refs.expandedTimeline.clientHeight;
      } else {
        this.videoHeight = 'auto';
      }
    });
  },
  methods: {
    toggleSearch() {
      this.search = !(this.search);
      this.buttonClicked("Toggle Search Bar");
    },
    /**
     * determines whether left/right arrow keys should move cursor in input field
     * and upon click of input field, fills with current timestamp
     */
    updateInputMode(isInputMode) {
      this.isSeekTimeInputMode = isInputMode;
      this.store.isInputMode = isInputMode;
      if (!isInputMode) {
        this.searchInput = "";
      } else {
        this.searchInput = this.seekTime;
      }
    },
    /** Navigates to closest timestamp in timeline to search input*/
    updateSearchForTimestamp() {
      const closestTimestamp = getClosestTimestamp(this.searchInput, this.mergedTimeline.timeline);
      this.$store.dispatch("updateTimelineTime", closestTimestamp);
      this.updateInputMode(false);
      this.newEventOccurred("Searching for timestamp")
    },

    emitBottomHeightUpdate() {
      if (this.$refs.bottomNav) {
        const newHeight = this.$refs.bottomNav.$el.clientHeight;
        this.$emit('bottom-nav-height-change', newHeight);
      }
    },
    computeMergedTimeline() {
      const mergedTimeline = {
        timeline: [], // Array of integers timestamps
        selectedIndex: 0,
      };

      const timelineIndexes = [];
      const timelines = [];
      for (const file of this.timelineFiles) {
        timelineIndexes.push(0);
        timelines.push(file.timeline);
      }

      var timelineToAdvance = 0;
      while (timelineToAdvance !== undefined) {
        timelineToAdvance = undefined;
        let minTime = Infinity;

        for (let i = 0; i < timelines.length; i++) {
          const timeline = timelines[i];
          const index = timelineIndexes[i];

          if (index >= timeline.length) {
            continue;
          }

          const time = timeline[index];

          if (time < minTime) {
            minTime = time;
            timelineToAdvance = i;
          }
        }

        if (timelineToAdvance === undefined) {
          // No more elements left
          break;
        }

        timelineIndexes[timelineToAdvance]++;
        mergedTimeline.timeline.push(minTime);
      }

      // Object is frozen for performance reasons
      // It will prevent Vue from making it a reactive object which will be very
      // slow as the timeline gets larger.
      Object.freeze(mergedTimeline.timeline);

      return mergedTimeline;
    },
    toggle() {
      this.minimized ? this.expand() : this.minimize();

      this.minimized = !this.minimized;
    },
    expand() {
      if (this.video) {
        this.$refs.expandedContentVideoContainer
            .appendChild(this.$refs.video.$el);
      }
    },
    minimize() {
      if (this.video) {
        this.$refs.overlayVideoContainer.appendChild(this.$refs.video.$el);
      }
    },
    fileIsVisible(f) {
      return this.visibleDataViews.includes(f.filename);
    },
    resizeBottomNav(e) {
      this.initResizeAction(e);
    },
    initResizeAction(e) {
      document.onmousemove = this.startResize;
      document.onmouseup = this.endResize;
    },
    startResize(e) {
      if (this.dragState.clientY === null) {
        this.dragState.clientY = e.clientY;
      }

      const movement = this.dragState.clientY - e.clientY;

      const resizeOffset = this.resizeOffset + movement;
      if (resizeOffset < 0) {
        this.resizeOffset = 0;
        this.dragState.clientY = null;
      } else if (movement > this.getBottomNavDistanceToTop()) {
        this.dragState.clientY += this.getBottomNavDistanceToTop();
        this.resizeOffset += this.getBottomNavDistanceToTop();
      } else {
        this.resizeOffset = resizeOffset;
        this.dragState.clientY = e.clientY;
      }
    },
    endResize() {
      this.dragState.lastDragEndPosition = this.dragState.clientY;
      this.dragState.clientY = null;
      document.onmouseup = null;
      document.onmousemove = null;
    },
    getBottomNavDistanceToTop() {
      return this.$refs.bottomNav.$el.getBoundingClientRect().top;
    },
    closeVideoOverlay() {
      this.showVideoOverlay = false;
      this.buttonClicked("Close Video Overlay")
    },
    openVideoOverlay() {
      this.showVideoOverlay = true;
      this.buttonClicked("Open Video Overlay")
    },
    toggleVideoOverlay() {
      this.showVideoOverlay = !this.showVideoOverlay;
      this.buttonClicked("Toggle Video Overlay")
    },
    videoLoaded() {
      this.$refs.videoOverlay.contentLoaded();
    },
    updateNavigationFileFilter() {
      if (!this.minimized) {
        // Always use custom mode navigation when timeline is expanded
        this.$store.commit('setNavigationFilesFilter',
            (f) => !f.timelineDisabled);
        return;
      }

      let navigationStyleFilter;
      switch (this.navigationStyle) {
        case NAVIGATION_STYLE.GLOBAL:
          navigationStyleFilter = (f) => true;
          break;

        case NAVIGATION_STYLE.FOCUSED:
          navigationStyleFilter =
            (f) => f.type === this.focusedFile.type;
          break;

        case NAVIGATION_STYLE.CUSTOM:
          navigationStyleFilter = (f) => !f.timelineDisabled;
          break;

        default:
          const split = this.navigationStyle.split('-');
          if (split[0] !== NAVIGATION_STYLE.TARGETED) {
            console.warn('Unexpected navigation type; fallback to global');
            navigationStyleFilter = (f) => true;
            break;
          }

          const fileType = split[1];
          navigationStyleFilter =
            (f) => f.type === fileType;
      }

      this.$store.commit('setNavigationFilesFilter', navigationStyleFilter);
    },
    updateVideoOverlayWidth(width) {
      this.videoOverlayExtraWidth = width;
    },
    onTimelineCrop(cropDetails) {
      this.crop = cropDetails;
    },
    onTimelineCropIntent(cropIntent) {
      this.cropIntent = cropIntent;
    },
    changeVideoTimestamp(ts) {
      if (!this.$refs.video) {
        return;
      }
      this.$refs.video.selectFrameAtTime(ts);
    },
    resetVideoTimestamp() {
      if (!this.$refs.video) {
        return;
      }
      this.$refs.video.jumpToSelectedIndex();
    },
    clearSelection() {
      this.crop = null;
    },
  },
  components: {
    'timeline': Timeline,
    'timelines': Timelines,
    'timeline-selection': TimelineSelection,
    'videoview': VideoView,
    'draggable-div': DraggableDiv,
    'md-icon-option': MdIconOption,
    'searchbar': Searchbar,
  },
};
</script>
<style scoped>
.overlay {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  width: 100vw;
  height: 100vh;
  z-index: 10;
  margin: 0;
  display: flex;
  flex-direction: column;
  pointer-events: none;
}

.overlay-content {
  flex-grow: 1;
}

.bottom-nav {
  background: white;
  margin: 0;
  max-height: 100vh;
  bottom: 0;
  left: 0;
  pointer-events: all;
}

.nav-content {
  width: 100%;
}

.toolbar, .active-timeline, .options {
  display: flex;
  flex-direction: row;
  flex: 1;
  align-items: center;
}

.toolbar.expanded {
  align-items: baseline;
}

.minimized-timeline-content {
 flex-grow: 1;
}

.minimized-timeline-content .seek-time {
  padding: 3px 0;
}

.options, .expanded-content .seek-time {
  padding: 0 20px 15px 20px;
}

.options label {
  font-weight: 600;
}

.options .datafilter {
  height: 50px;
  display: flex;
  align-items: center;
}

.expanded-content {
  display: flex;
}

.flex-fill {
  flex-grow: 1;
}

.video {
  flex-grow: 0;
}

.resize-bar {
  flex-grow: 1;
}

.drag-handle {
  cursor: grab;
}

.md-icon-button {
  margin: 0;
}

.toggle-btn {
  margin-left: 8px;
  align-self: flex-end;
}

.video-overlay {
  display: inline-block;
  margin-bottom: 15px;
  min-width: 50px;
  max-width: 50vw;
  height: auto;
  resize: horizontal;
  pointer-events: all;
}

.close-video-overlay {
  float: right;
  cursor: pointer;
}

.show-video-overlay-btn {
  margin-left: 12px;
  margin-right: -8px;
  align-self: flex-end;
}

.show-video-overlay-btn .md-icon {
  color: #9E9E9E!important;
}

.collapsed-timeline-icon {
  cursor: pointer;
}

.show-video-overlay-btn.active .md-icon {
  color: #212121!important;
}

.help {
  display: flex;
  align-content: flex-end;
  align-items: flex-end;
  flex-direction: column;
}

.help-icon-wrapper {
  margin-right: 20px;
  margin-bottom: 10px;
}

.help-icon-wrapper .help-icon {
  cursor: help;
}

.trace-icon {
  cursor: pointer;
  user-select: none;
}

.trace-icon.disabled {
  color: gray;
}

.active-timeline {
  flex: 0 0 auto;
}

.active-timeline .icon {
  margin-right: 20px;
}

.active-timeline .active-timeline-icon {
  margin-right: 10px;
  align-self: flex-end;
  margin-bottom: 18px;
}

.minimized-timeline-content {
  align-self: flex-start;
  padding-top: 1px;
}

.minimized-timeline-content label {
  color: rgba(0,0,0,0.54);
  font-size: 12px;
  font-family: inherit;
  cursor: text;
}

.minimized-timeline-content .minimized-timeline {
  margin-top: 4px;
}

.navigation-style-selection-field {
  width: 90px;
  margin-right: 10px;
  margin-bottom: 0;
}

.timeline-selection-header {
  display: flex;
  align-items: center;
  padding-left: 15px;
  height: 48px;
}

.help-icon {
  font-size: 15px;
  margin-bottom: 15px;
  cursor: help;
}

.timestamp-search-input {
  outline: none;
  border-width: 0 0 1px;
  border-color: gray;
  font-family: inherit;
  color: #448aff;
  font-size: 12px;
  padding: 0;
  letter-spacing: inherit;
  width: 125px;
}

.timestamp-search-input:focus {
  border-color: #448aff;
}

.timestamp-search-input.expanded {
  font-size: 14px;
  width: 150px;
}

.drop-search:hover {
  background-color: #9af39f;
}
</style>
