/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */

const SplitBox = require("resource://devtools/client/shared/components/splitter/SplitBox.js");

import React, { Component } from "devtools/client/shared/vendor/react";
import {
  div,
  input,
  label,
  button,
  a,
} from "devtools/client/shared/vendor/react-dom-factories";
import PropTypes from "devtools/client/shared/vendor/react-prop-types";
import { connect } from "devtools/client/shared/vendor/react-redux";

import actions from "../../actions/index";
import {
  getTopFrame,
  getExpressions,
  isMapScopesEnabled,
  getSelectedFrame,
  getSelectedSource,
  getThreads,
  getCurrentThread,
  getPauseReason,
  getShouldBreakpointsPaneOpenOnPause,
  getSkipPausing,
  shouldLogEventBreakpoints,
} from "../../selectors/index";

import DebuggerImage from "devtools/client/shared/components/DebuggerImage";
import { prefs } from "../../utils/prefs";

import Breakpoints from "./Breakpoints/index";
import Expressions from "./Expressions";
import Frames from "./Frames/index";
import Threads from "./Threads";
import Accordion from "../shared/Accordion";
import CommandBar from "./CommandBar";
import XHRBreakpoints from "./XHRBreakpoints";
import EventListeners from "../shared/EventListeners";
import DOMMutationBreakpoints from "./DOMMutationBreakpoints";
import WhyPaused from "./WhyPaused";

import Scopes from "./Scopes";

const classnames = require("resource://devtools/client/shared/classnames.js");

function debugBtn(onClick, type, className, tooltip) {
  return button(
    {
      onClick,
      className: `${type} ${className}`,
      key: type,
      title: tooltip,
    },
    React.createElement(DebuggerImage, {
      name: type,
      title: tooltip,
      "aria-label": tooltip,
    })
  );
}

const mdnLink =
  "https://firefox-source-docs.mozilla.org/devtools-user/debugger/using_the_debugger_map_scopes_feature/";

class SecondaryPanes extends Component {
  constructor(props) {
    super(props);

    this.state = {
      showExpressionsInput: false,
      showXHRInput: false,
      expandedFrameGroups: {},
    };
  }

  static get propTypes() {
    return {
      evaluateExpressionsForCurrentContext: PropTypes.func.isRequired,
      expressions: PropTypes.array.isRequired,
      hasFrames: PropTypes.bool.isRequired,
      horizontal: PropTypes.bool.isRequired,
      logEventBreakpoints: PropTypes.bool.isRequired,
      mapScopesEnabled: PropTypes.bool.isRequired,
      pauseReason: PropTypes.string.isRequired,
      shouldBreakpointsPaneOpenOnPause: PropTypes.bool.isRequired,
      thread: PropTypes.string,
      skipPausing: PropTypes.bool.isRequired,
      showScopesButtons: PropTypes.bool.isRequired,
      toggleEventLogging: PropTypes.func.isRequired,
      resetBreakpointsPaneState: PropTypes.func.isRequired,
      toggleMapScopes: PropTypes.func.isRequired,
      showThreads: PropTypes.bool.isRequired,
      removeAllBreakpoints: PropTypes.func.isRequired,
      removeAllXHRBreakpoints: PropTypes.func.isRequired,
    };
  }

  onExpressionAdded = () => {
    this.setState({ showExpressionsInput: false });
  };

  onXHRAdded = () => {
    this.setState({ showXHRInput: false });
  };

  onExpandFrameGroup = expandedFrameGroups => {
    this.setState({
      expandedFrameGroups: { ...expandedFrameGroups },
    });
  };

  watchExpressionHeaderButtons() {
    const { expressions } = this.props;
    const buttons = [];

    if (expressions.length) {
      buttons.push(
        debugBtn(
          () => {
            this.props.evaluateExpressionsForCurrentContext();
          },
          "refresh",
          "active",
          L10N.getStr("watchExpressions.refreshButton")
        )
      );
    }
    buttons.push(
      debugBtn(
        () => {
          if (!prefs.expressionsVisible) {
            this.onWatchExpressionPaneToggle(true);
          }
          this.setState({ showExpressionsInput: true });
        },
        "plus",
        "active",
        L10N.getStr("expressions.placeholder2")
      )
    );
    return buttons;
  }

  xhrBreakpointsHeaderButtons() {
    return [
      debugBtn(
        () => {
          if (!prefs.xhrBreakpointsVisible) {
            this.onXHRPaneToggle(true);
          }
          this.setState({ showXHRInput: true });
        },
        "plus",
        "active",
        L10N.getStr("xhrBreakpoints.label")
      ),

      debugBtn(
        () => {
          this.props.removeAllXHRBreakpoints();
        },
        "removeAll",
        "active",
        L10N.getStr("xhrBreakpoints.removeAll.tooltip")
      ),
    ];
  }

  breakpointsHeaderButtons() {
    return [
      debugBtn(
        () => {
          this.props.removeAllBreakpoints();
        },
        "removeAll",
        "active",
        L10N.getStr("breakpointMenuItem.deleteAll")
      ),
    ];
  }

  getScopeItem() {
    return {
      header: L10N.getStr("scopes.header"),
      className: "scopes-pane",
      id: "scopes-pane",
      component: React.createElement(Scopes, null),
      opened: prefs.scopesVisible,
      buttons: this.getScopesButtons(),
      onToggle: opened => {
        prefs.scopesVisible = opened;
      },
    };
  }

  getScopesButtons() {
    if (!this.props.showScopesButtons) {
      return null;
    }
    const { mapScopesEnabled } = this.props;

    return [
      div(
        {
          key: "scopes-buttons",
        },
        label(
          {
            className: "map-scopes-header",
            title: L10N.getStr("scopes.showOriginalScopesTooltip"),
            onClick: e => e.stopPropagation(),
          },
          input({
            type: "checkbox",
            checked: mapScopesEnabled ? "checked" : "",
            onChange: () => this.props.toggleMapScopes(),
          }),
          L10N.getStr("scopes.showOriginalScopes")
        ),
        a(
          {
            className: "mdn",
            target: "_blank",
            href: mdnLink,
            onClick: e => e.stopPropagation(),
            title: L10N.getStr("scopes.showOriginalScopesHelpTooltip"),
          },
          React.createElement(DebuggerImage, {
            name: "shortcuts",
          })
        )
      ),
    ];
  }

  getEventButtons() {
    const { logEventBreakpoints } = this.props;
    return [
      div(
        {
          key: "events-buttons",
        },
        label(
          {
            className: "events-header",
            title: L10N.getStr("eventlisteners.log.label"),
          },
          input({
            type: "checkbox",
            checked: logEventBreakpoints ? "checked" : "",
            onChange: () => this.props.toggleEventLogging(),
          }),
          L10N.getStr("eventlisteners.log")
        )
      ),
    ];
  }

  onWatchExpressionPaneToggle(opened) {
    prefs.expressionsVisible = opened;
  }

  getWatchItem() {
    return {
      header: L10N.getStr("watchExpressions.header"),
      id: "watch-expressions-pane",
      className: "watch-expressions-pane",
      buttons: this.watchExpressionHeaderButtons(),
      component: React.createElement(Expressions, {
        showInput: this.state.showExpressionsInput,
        onExpressionAdded: this.onExpressionAdded,
      }),
      opened: prefs.expressionsVisible,
      onToggle: this.onWatchExpressionPaneToggle,
    };
  }

  onXHRPaneToggle(opened) {
    prefs.xhrBreakpointsVisible = opened;
  }

  getXHRItem() {
    const { pauseReason } = this.props;

    return {
      header: L10N.getStr("xhrBreakpoints.header"),
      id: "xhr-breakpoints-pane",
      className: "xhr-breakpoints-pane",
      buttons: this.xhrBreakpointsHeaderButtons(),
      component: React.createElement(XHRBreakpoints, {
        showInput: this.state.showXHRInput,
        onXHRAdded: this.onXHRAdded,
      }),
      opened: prefs.xhrBreakpointsVisible || pauseReason === "XHR",
      onToggle: this.onXHRPaneToggle,
    };
  }

  getCallStackItem() {
    return {
      header: L10N.getStr("callStack.header"),
      id: "call-stack-pane",
      className: "call-stack-pane",
      component: React.createElement(Frames, {
        panel: "debugger",
        // These props enable storing and using the current expanded state
        // of the frame groups. This is we always handle displaying selected frames
        // in groups correctly.
        onExpandFrameGroup: this.onExpandFrameGroup,
        expandedFrameGroups: this.state.expandedFrameGroups,
      }),
      opened: prefs.callStackVisible,
      onToggle: opened => {
        prefs.callStackVisible = opened;
      },
    };
  }

  getThreadsItem() {
    return {
      header: L10N.getStr("threadsHeader"),
      id: "threads-pane",
      className: "threads-pane",
      component: React.createElement(Threads, null),
      opened: prefs.threadsVisible,
      onToggle: opened => {
        prefs.threadsVisible = opened;
      },
    };
  }

  getBreakpointsItem() {
    const { pauseReason, shouldBreakpointsPaneOpenOnPause, thread } =
      this.props;

    return {
      header: L10N.getStr("breakpoints.header"),
      id: "breakpoints-pane",
      className: "breakpoints-pane",
      buttons: this.breakpointsHeaderButtons(),
      component: React.createElement(Breakpoints),
      opened:
        prefs.breakpointsVisible ||
        (pauseReason === "breakpoint" && shouldBreakpointsPaneOpenOnPause),
      onToggle: opened => {
        prefs.breakpointsVisible = opened;
        //  one-shot flag used to force open the Breakpoints Pane only
        //  when hitting a breakpoint, but not when selecting frames etc...
        if (shouldBreakpointsPaneOpenOnPause) {
          this.props.resetBreakpointsPaneState(thread);
        }
      },
    };
  }

  getEventListenersItem() {
    const { pauseReason } = this.props;

    return {
      header: L10N.getStr("eventListenersHeader1"),
      id: "event-listeners-pane",
      className: "event-listeners-pane",
      buttons: this.getEventButtons(),
      component: React.createElement(EventListeners, {
        panelKey: "breakpoint",
      }),
      opened: prefs.eventListenersVisible || pauseReason === "eventBreakpoint",
      onToggle: opened => {
        prefs.eventListenersVisible = opened;
      },
    };
  }

  getDOMMutationsItem() {
    const { pauseReason } = this.props;

    return {
      header: L10N.getStr("domMutationHeader"),
      id: "dom-mutations-pane",
      className: "dom-mutations-pane",
      buttons: [],
      component: React.createElement(DOMMutationBreakpoints, null),
      opened:
        prefs.domMutationBreakpointsVisible ||
        pauseReason === "mutationBreakpoint",
      onToggle: opened => {
        prefs.domMutationBreakpointsVisible = opened;
      },
    };
  }

  getStartItems() {
    const items = [];
    const { horizontal, hasFrames } = this.props;

    if (horizontal) {
      if (this.props.showThreads) {
        items.push(this.getThreadsItem());
      }

      items.push(this.getWatchItem());
    }

    items.push(this.getBreakpointsItem());

    if (hasFrames) {
      items.push(this.getCallStackItem());
      if (horizontal) {
        items.push(this.getScopeItem());
      }
    }

    items.push(this.getXHRItem());

    items.push(this.getEventListenersItem());

    items.push(this.getDOMMutationsItem());

    return items;
  }

  getEndItems() {
    if (this.props.horizontal) {
      return [];
    }

    const items = [];
    if (this.props.showThreads) {
      items.push(this.getThreadsItem());
    }

    items.push(this.getWatchItem());

    if (this.props.hasFrames) {
      items.push(this.getScopeItem());
    }

    return items;
  }

  getItems() {
    return [...this.getStartItems(), ...this.getEndItems()];
  }

  renderHorizontalLayout() {
    return div(
      null,
      React.createElement(WhyPaused),
      React.createElement(Accordion, {
        items: this.getItems(),
      })
    );
  }

  renderVerticalLayout() {
    return React.createElement(SplitBox, {
      initialSize: "300px",
      minSize: 10,
      maxSize: "50%",
      splitterSize: 1,
      startPanel: div(
        {
          style: {
            width: "inherit",
          },
        },
        React.createElement(WhyPaused),
        React.createElement(Accordion, {
          items: this.getStartItems(),
        })
      ),
      endPanel: React.createElement(Accordion, {
        items: this.getEndItems(),
      }),
    });
  }

  render() {
    const { skipPausing } = this.props;
    return div(
      {
        className: "secondary-panes-wrapper",
      },
      React.createElement(CommandBar, {
        horizontal: this.props.horizontal,
      }),
      React.createElement(
        "div",
        {
          className: classnames(
            "secondary-panes",
            skipPausing && "skip-pausing"
          ),
        },
        this.props.horizontal
          ? this.renderHorizontalLayout()
          : this.renderVerticalLayout()
      )
    );
  }
}

const mapStateToProps = state => {
  const thread = getCurrentThread(state);
  const selectedFrame = getSelectedFrame(state);
  const selectedSource = getSelectedSource(state);
  const pauseReason = getPauseReason(state, thread);
  const shouldBreakpointsPaneOpenOnPause = getShouldBreakpointsPaneOpenOnPause(
    state,
    thread
  );

  return {
    expressions: getExpressions(state),
    hasFrames: !!getTopFrame(state, thread),
    mapScopesEnabled: isMapScopesEnabled(state),
    showThreads: !!getThreads(state).length,
    skipPausing: getSkipPausing(state),
    logEventBreakpoints: shouldLogEventBreakpoints(state),
    showScopesButtons:
      selectedFrame &&
      selectedSource &&
      selectedSource.isOriginal &&
      !selectedSource.isPrettyPrinted,
    pauseReason: pauseReason?.type ?? "",
    shouldBreakpointsPaneOpenOnPause,
    thread,
  };
};

export default connect(mapStateToProps, {
  evaluateExpressionsForCurrentContext:
    actions.evaluateExpressionsForCurrentContext,
  toggleMapScopes: actions.toggleMapScopes,
  breakOnNext: actions.breakOnNext,
  toggleEventLogging: actions.toggleEventLogging,
  removeAllBreakpoints: actions.removeAllBreakpoints,
  removeAllXHRBreakpoints: actions.removeAllXHRBreakpoints,
  resetBreakpointsPaneState: actions.resetBreakpointsPaneState,
})(SecondaryPanes);
