// 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 './tree.scss';
import m from 'mithril';
import {classNames} from '../base/classnames';
import {hasChildren} from '../base/mithril_utils';

// Heirachical tree layout with left and right values.
// Right and left values of the same indentation level are horizontally aligned.
// Example:
// foo    bar
//  ├ baz  qux
//  └ quux corge
//    ├ looong_left aaa
//    └ a           bbb
// grault garply

interface TreeAttrs {
  // Space delimited class list applied to our tree element.
  className?: string;
  // When true, render the tree as a bordered card with a divider between
  // every label/value row and a tinted label column. Use for stats/key-value
  // tables where rows should look like proper cells.
  bordered?: boolean;
}

export class Tree implements m.ClassComponent<TreeAttrs> {
  view({attrs, children}: m.Vnode<TreeAttrs>): m.Children {
    const {className = '', bordered} = attrs;

    const classes = classNames(className, bordered && 'pf-tree--bordered');

    return m('.pf-tree', {class: classes}, children);
  }
}

interface TreeNodeAttrs {
  // Content to display in the left hand column.
  // If omitted, this side will be blank.
  left?: m.Children;
  // Content to display in the right hand column.
  // If omitted, this side will be left blank.
  right?: m.Children;
  // Content to display in the right hand column when the node is collapsed.
  // If omitted, the value of `right` shall be shown when collapsed instead.
  // If the node has no children, this value is never shown.
  summary?: m.Children;
  // Whether this node is collapsed or not.
  // If omitted, collapsed state 'uncontrolled' - i.e. controlled internally.
  collapsed?: boolean;
  // Whether the node should start collapsed or not, default: false.
  startsCollapsed?: boolean;
  loading?: boolean;
  showCaret?: boolean;
  // Optional icon to show to the left of the text.
  // If this node contains children, this icon is ignored.
  icon?: string;
  // Called when the collapsed state is changed, mainly used in controlled mode.
  onCollapseChanged?: (collapsed: boolean, attrs: TreeNodeAttrs) => void;
}

export class TreeNode implements m.ClassComponent<TreeNodeAttrs> {
  private collapsed;

  constructor({attrs}: m.CVnode<TreeNodeAttrs>) {
    this.collapsed = attrs.startsCollapsed ?? false;
  }

  view(vnode: m.CVnode<TreeNodeAttrs>): m.Children {
    const {
      children,
      attrs,
      attrs: {left, onCollapseChanged = () => {}},
    } = vnode;
    return [
      m(
        '.pf-tree-node',
        {
          class: classNames(this.getClassNameForNode(vnode)),
        },
        m(
          '.pf-tree-left',
          m('span.pf-tree-gutter', {
            onclick: () => {
              this.collapsed = !this.isCollapsed(vnode);
              onCollapseChanged(this.collapsed, attrs);
            },
          }),
          left,
        ),
        this.renderRight(vnode),
      ),
      hasChildren(vnode) && m('.pf-tree-children', children),
    ];
  }

  private getClassNameForNode(vnode: m.CVnode<TreeNodeAttrs>) {
    const {loading = false, showCaret = false} = vnode.attrs;
    if (loading) {
      return 'pf-loading';
    } else if (hasChildren(vnode) || showCaret) {
      if (this.isCollapsed(vnode)) {
        return 'pf-collapsed';
      } else {
        return 'pf-expanded';
      }
    } else {
      return undefined;
    }
  }

  private renderRight(vnode: m.CVnode<TreeNodeAttrs>) {
    const {
      attrs: {right, summary},
    } = vnode;
    if (hasChildren(vnode) && this.isCollapsed(vnode)) {
      return m('.pf-tree-right', summary ?? right);
    } else {
      return m('.pf-tree-right', right);
    }
  }

  private isCollapsed({attrs}: m.Vnode<TreeNodeAttrs>): boolean {
    // If collapsed is omitted, use our local collapsed state instead.
    const {collapsed = this.collapsed} = attrs;

    return collapsed;
  }
}

interface LazyTreeNodeAttrs {
  // Same as TreeNode (see above).
  left?: m.Children;
  // Same as TreeNode (see above).
  right?: m.Children;
  // Same as TreeNode (see above).
  icon?: string;
  // Same as TreeNode (see above).
  summary?: m.Children;
  // A callback to be called when the TreeNode is expanded, in order to fetch
  // child nodes.
  // The callback must return a promise to a function which returns m.Children.
  // The reason the promise must return a function rather than the actual
  // children is to avoid storing vnodes between render cycles, which is a bug
  // in Mithril.
  fetchData: () => Promise<() => m.Children>;
  // Whether to unload children on collapse.
  // Defaults to false, data will be kept in memory until the node is destroyed.
  unloadOnCollapse?: boolean;
}

// This component is a TreeNode which only loads child nodes when it's expanded.
// This allows us to represent huge trees without having to load all the data
// up front, and even allows us to represent infinite or recursive trees.
export class LazyTreeNode implements m.ClassComponent<LazyTreeNodeAttrs> {
  private collapsed: boolean = true;
  private loading: boolean = false;
  private renderChildren?: () => m.Children;

  view({attrs}: m.CVnode<LazyTreeNodeAttrs>): m.Children {
    const {
      left,
      right,
      icon,
      summary,
      fetchData,
      unloadOnCollapse = false,
    } = attrs;

    return m(
      TreeNode,
      {
        left,
        right,
        icon,
        summary,
        showCaret: true,
        loading: this.loading,
        collapsed: this.collapsed,
        onCollapseChanged: (collapsed) => {
          if (collapsed) {
            if (unloadOnCollapse) {
              this.renderChildren = undefined;
            }
          } else {
            // Expanding
            if (this.renderChildren) {
              this.collapsed = false;
            } else {
              this.loading = true;
              fetchData().then((result) => {
                this.loading = false;
                this.collapsed = false;
                this.renderChildren = result;
              });
            }
          }
          this.collapsed = collapsed;
        },
      },
      this.renderChildren && this.renderChildren(),
    );
  }
}
