// Copyright (C) 2019 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 m from 'mithril';

import {time, Time} from '../base/time';
import {Actions} from '../common/actions';
import {HighPrecisionTimeSpan} from '../common/high_precision_time';
import {
  LogBounds,
  LogBoundsKey,
  LogEntries,
  LogEntriesKey,
} from '../common/logs';
import {raf} from '../core/raf_scheduler';
import {DetailsShell} from '../widgets/details_shell';
import {VirtualScrollContainer} from '../widgets/virtual_scroll_container';

import {SELECTED_LOG_ROWS_COLOR} from './css_constants';
import {globals} from './globals';
import {LOG_PRIORITIES, LogsFilters} from './logs_filters';
import {Timestamp} from './widgets/timestamp';

const ROW_H = 20;

export class LogPanel implements m.ClassComponent {
  private bounds?: LogBounds;
  private entries?: LogEntries;

  private visibleRowOffset = 0;
  private visibleRowCount = 0;

  recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
    const prevOffset = this.visibleRowOffset;
    const prevCount = this.visibleRowCount;
    this.visibleRowOffset = Math.floor(scrollContainer.scrollTop / ROW_H);
    this.visibleRowCount = Math.ceil(scrollContainer.clientHeight / ROW_H);

    if (this.visibleRowOffset !== prevOffset ||
        this.visibleRowCount !== prevCount) {
      globals.dispatch(Actions.updateLogsPagination({
        offset: this.visibleRowOffset,
        count: this.visibleRowCount,
      }));
    }
  }

  oncreate(_: m.CVnodeDOM) {
    // TODO(stevegolton): Type assersions are a source of bugs.
    // Let's try to find another way of doing this.
    this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
    this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
  }

  onbeforeupdate(_: m.CVnode) {
    // TODO(stevegolton): Type assersions are a source of bugs.
    // Let's try to find another way of doing this.
    this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
    this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
  }

  onScroll = (scrollContainer: HTMLElement) => {
    this.recomputeVisibleRowsAndUpdate(scrollContainer);
    raf.scheduleFullRedraw();
  };

  onRowOver(ts: time) {
    globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
  }

  onRowOut() {
    globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
  }

  private totalRows():
      {isStale: boolean, total: number, offset: number, count: number} {
    if (!this.bounds) {
      return {isStale: false, total: 0, offset: 0, count: 0};
    }
    const {
      totalVisibleLogs,
      firstVisibleLogTs,
      lastVisibleLogTs,
    } = this.bounds;
    const vis = globals.timeline.visibleWindowTime;

    const visibleLogSpan =
        new HighPrecisionTimeSpan(firstVisibleLogTs, lastVisibleLogTs);
    const isStale = !vis.contains(visibleLogSpan);
    const offset = Math.min(this.visibleRowOffset, totalVisibleLogs);
    const visCount = Math.min(totalVisibleLogs - offset, this.visibleRowCount);
    return {isStale, total: totalVisibleLogs, count: visCount, offset};
  }

  view(_: m.CVnode<{}>) {
    const {isStale, total, offset, count} = this.totalRows();

    const hasProcessNames = this.entries &&
        this.entries.processName.filter((name) => name).length > 0;

    const rows: m.Children = [];
    rows.push(
        m(`.row`,
          m('.cell.row-header', 'Timestamp'),
          m('.cell.row-header', 'Level'),
          m('.cell.row-header', 'Tag'),
          hasProcessNames ? m('.cell.with-process.row-header', 'Process name') :
                            undefined,
          hasProcessNames ? m('.cell.with-process.row-header', 'Message') :
                            m('.cell.no-process.row-header', 'Message'),
          m('br')));
    if (this.entries) {
      const offset = this.entries.offset;
      const timestamps = this.entries.timestamps;
      const priorities = this.entries.priorities;
      const tags = this.entries.tags;
      const messages = this.entries.messages;
      const processNames = this.entries.processName;
      for (let i = 0; i < this.entries.timestamps.length; i++) {
        const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
        const ts = timestamps[i];
        const prioClass = priorityLetter || '';
        const style: {top: string, backgroundColor?: string} = {
          // 1.5 is for the width of the header
          top: `${(offset + i + 1.5) * ROW_H}px`,
        };
        if (this.entries.isHighlighted[i]) {
          style.backgroundColor = SELECTED_LOG_ROWS_COLOR;
        }

        rows.push(
            m(`.row.${prioClass}`,
              {
                'class': isStale ? 'stale' : '',
                style,
                'onmouseover': this.onRowOver.bind(this, ts),
                'onmouseout': this.onRowOut.bind(this),
              },
              m('.cell', m(Timestamp, {ts})),
              m('.cell', priorityLetter || '?'),
              m('.cell', tags[i]),
              hasProcessNames ? m('.cell.with-process', processNames[i]) :
                                undefined,
              hasProcessNames ? m('.cell.with-process', messages[i]) :
                                m('.cell.no-process', messages[i]),
              m('br')));
      }
    }

    // TODO(stevegolton): Add a 'loading' state to DetailsShell, which shows a
    // scrolling scrolly bar at the bottom of the banner & map isStale to it
    return m(
        DetailsShell,
        {
          title: 'Android Logs',
          description: `[${offset}, ${offset + count}] / ${total}`,
          buttons: m(LogsFilters),
        },
        m(
            VirtualScrollContainer,
            {onScroll: this.onScroll},
            m('.log-panel',
              m('.rows', {style: {height: `${total * ROW_H}px`}}, rows)),
            ),
    );
  }
}
