/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
/* eslint camelcase: 0 */
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { styled, t, supersetTheme, css } from '@superset-ui/core';
import { debounce } from 'lodash';

import { useDynamicPluginContext } from 'src/components/DynamicPlugins';
import { Global } from '@emotion/core';
import { Tooltip } from 'src/common/components/Tooltip';
import { usePrevious } from 'src/common/hooks/usePrevious';
import Icon from 'src/components/Icon';
import ExploreChartPanel from './ExploreChartPanel';
import ConnectedControlPanelsContainer from './ControlPanelsContainer';
import SaveModal from './SaveModal';
import QueryAndSaveBtns from './QueryAndSaveBtns';
import DataSourcePanel from './DatasourcePanel';
import { getExploreLongUrl } from '../exploreUtils';
import { areObjectsEqual } from '../../reduxUtils';
import { getFormDataFromControls } from '../controlUtils';
import { chartPropShape } from '../../dashboard/util/propShapes';
import * as exploreActions from '../actions/exploreActions';
import * as saveModalActions from '../actions/saveModalActions';
import * as chartActions from '../../chart/chartAction';
import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
import * as logActions from '../../logger/actions';
import {
  LOG_ACTIONS_MOUNT_EXPLORER,
  LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS,
} from '../../logger/LogUtils';

const propTypes = {
  ...ExploreChartPanel.propTypes,
  height: PropTypes.string,
  width: PropTypes.string,
  actions: PropTypes.object.isRequired,
  datasource_type: PropTypes.string.isRequired,
  dashboardId: PropTypes.number,
  isDatasourceMetaLoading: PropTypes.bool.isRequired,
  chart: chartPropShape.isRequired,
  slice: PropTypes.object,
  sliceName: PropTypes.string,
  controls: PropTypes.object.isRequired,
  forcedHeight: PropTypes.string,
  form_data: PropTypes.object.isRequired,
  standalone: PropTypes.bool.isRequired,
  timeout: PropTypes.number,
  impressionId: PropTypes.string,
  vizType: PropTypes.string,
};

const Styles = styled.div`
  background: ${({ theme }) => theme.colors.grayscale.light5};
  text-align: left;
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: stretch;
  border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
  .explore-column {
    display: flex;
    flex: 0 0 ${({ theme }) => theme.gridUnit * 80}px;
    flex-direction: column;
    padding: ${({ theme }) => 2 * theme.gridUnit}px 0;
    max-width: ${({ theme }) => theme.gridUnit * 80}px;
    max-height: 100%;
  }
  .data-source-selection {
    background-color: ${({ theme }) => theme.colors.grayscale.light4};
    padding: ${({ theme }) => 2 * theme.gridUnit}px 0;
    border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
  }
  .main-explore-content {
    border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
  }
  .controls-column {
    align-self: flex-start;
    padding: 0;
  }
  .title-container {
    position: relative;
    display: flex;
    flex-direction: row;
    padding: 0 ${({ theme }) => 2 * theme.gridUnit}px;
    justify-content: space-between;
    .horizontal-text {
      text-transform: uppercase;
      color: ${({ theme }) => theme.colors.grayscale.light1};
      font-size: ${({ theme }) => 4 * theme.typography.sizes.s};
    }
  }
  .no-show {
    display: none;
  }
  .vertical-text {
    writing-mode: vertical-rl;
    text-orientation: mixed;
  }
  .sidebar {
    height: 100%;
    background-color: ${({ theme }) => theme.colors.grayscale.light4};
    padding: ${({ theme }) => 2 * theme.gridUnit}px;
    width: ${({ theme }) => 8 * theme.gridUnit}px;
  }
  .callpase-icon > svg {
    color: ${({ theme }) => theme.colors.primary.base};
  }
`;

const getWindowSize = () => ({
  height: window.innerHeight,
  width: window.innerWidth,
});

function useWindowSize({ delayMs = 250 } = {}) {
  const [size, setSize] = useState(getWindowSize());

  useEffect(() => {
    const onWindowResize = debounce(() => setSize(getWindowSize()), delayMs);
    window.addEventListener('resize', onWindowResize);
    return () => window.removeEventListener('resize', onWindowResize);
  }, []);

  return size;
}

function ExploreViewContainer(props) {
  const dynamicPluginContext = useDynamicPluginContext();
  const dynamicPlugin = dynamicPluginContext.plugins[props.vizType];
  const isDynamicPluginLoading = dynamicPlugin && dynamicPlugin.loading;
  const wasDynamicPluginLoading = usePrevious(isDynamicPluginLoading);

  const previousControls = usePrevious(props.controls);
  const windowSize = useWindowSize();

  const [showingModal, setShowingModal] = useState(false);
  const [chartIsStale, setChartIsStale] = useState(false);
  const [isCollapsed, setIsCollapsed] = useState(false);

  const width = `${windowSize.width}px`;
  const navHeight = props.standalone ? 0 : 90;
  const height = props.forcedHeight
    ? `${props.forcedHeight}px`
    : `${windowSize.height - navHeight}px`;

  function addHistory({ isReplace = false, title } = {}) {
    const payload = { ...props.form_data };
    const longUrl = getExploreLongUrl(props.form_data, null, false);
    try {
      if (isReplace) {
        window.history.replaceState(payload, title, longUrl);
      } else {
        window.history.pushState(payload, title, longUrl);
      }
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn(
        'Failed at altering browser history',
        payload,
        title,
        longUrl,
      );
    }
  }

  function handlePopstate() {
    const formData = window.history.state;
    if (formData && Object.keys(formData).length) {
      props.actions.setExploreControls(formData);
      props.actions.postChartFormData(
        formData,
        false,
        props.timeout,
        props.chart.id,
      );
    }
  }

  function onQuery() {
    // remove alerts when query
    props.actions.removeControlPanelAlert();
    props.actions.triggerQuery(true, props.chart.id);

    setChartIsStale(false);
    addHistory();
  }

  function handleKeydown(event) {
    const controlOrCommand = event.ctrlKey || event.metaKey;
    if (controlOrCommand) {
      const isEnter = event.key === 'Enter' || event.keyCode === 13;
      const isS = event.key === 's' || event.keyCode === 83;
      if (isEnter) {
        onQuery();
      } else if (isS) {
        if (props.slice) {
          props.actions
            .saveSlice(props.form_data, {
              action: 'overwrite',
              slice_id: props.slice.slice_id,
              slice_name: props.slice.slice_name,
              add_to_dash: 'noSave',
              goto_dash: false,
            })
            .then(({ data }) => {
              window.location = data.slice.slice_url;
            });
        }
      }
    }
  }

  function onStop() {
    if (props.chart && props.chart.queryController) {
      props.chart.queryController.abort();
    }
  }

  function toggleModal() {
    setShowingModal(!showingModal);
  }

  function toggleCollapse() {
    setIsCollapsed(!isCollapsed);
  }

  // effect to run on mount
  useEffect(() => {
    props.actions.logEvent(LOG_ACTIONS_MOUNT_EXPLORER);
    addHistory({ isReplace: true });
    window.addEventListener('popstate', handlePopstate);
    document.addEventListener('keydown', handleKeydown);
    return () => {
      window.removeEventListener('popstate', handlePopstate);
      document.removeEventListener('keydown', handleKeydown);
    };
  }, []);

  useEffect(() => {
    if (wasDynamicPluginLoading && !isDynamicPluginLoading) {
      // reload the controls now that we actually have the control config
      props.actions.dynamicPluginControlsReady();
    }
  }, [isDynamicPluginLoading]);

  // effect to run when controls change
  useEffect(() => {
    const hasError = Object.values(props.controls).some(
      control =>
        control.validationErrors && control.validationErrors.length > 0,
    );
    if (!hasError) {
      props.actions.triggerQuery(true, props.chart.id);
    }

    if (previousControls) {
      if (props.controls.viz_type.value !== previousControls.viz_type.value) {
        props.actions.resetControls();
      }
      if (
        props.controls.datasource &&
        (previousControls.datasource == null ||
          props.controls.datasource.value !== previousControls.datasource.value)
      ) {
        // this should really be handled by actions
        fetchDatasourceMetadata(props.form_data.datasource, true);
      }

      const changedControlKeys = Object.keys(props.controls).filter(
        key =>
          typeof previousControls[key] !== 'undefined' &&
          !areObjectsEqual(
            props.controls[key].value,
            previousControls[key].value,
          ),
      );

      // this should also be handled by the actions that are actually changing the controls
      const hasDisplayControlChanged = changedControlKeys.some(
        key => props.controls[key].renderTrigger,
      );
      if (hasDisplayControlChanged) {
        props.actions.updateQueryFormData(
          getFormDataFromControls(props.controls),
          props.chart.id,
        );
        props.actions.renderTriggered(new Date().getTime(), props.chart.id);
        addHistory();
      }

      // this should be handled inside actions too
      const hasQueryControlChanged = changedControlKeys.some(
        key =>
          !props.controls[key].renderTrigger &&
          !props.controls[key].dontRefreshOnChange,
      );
      if (hasQueryControlChanged) {
        props.actions.logEvent(LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS);
        setChartIsStale(true);
      }
    }
  }, [props.controls]);

  function renderErrorMessage() {
    // Returns an error message as a node if any errors are in the store
    const errors = Object.entries(props.controls)
      .filter(
        ([, control]) =>
          control.validationErrors && control.validationErrors.length > 0,
      )
      .map(([key, control]) => (
        <div key={key}>
          {t('Control labeled ')}
          <strong>{` "${control.label}" `}</strong>
          {control.validationErrors.join('. ')}
        </div>
      ));
    let errorMessage;
    if (errors.length > 0) {
      errorMessage = <div style={{ textAlign: 'left' }}>{errors}</div>;
    }
    return errorMessage;
  }

  function renderChartContainer() {
    return (
      <ExploreChartPanel
        width={width}
        height={height}
        {...props}
        errorMessage={renderErrorMessage()}
        refreshOverlayVisible={chartIsStale}
        addHistory={addHistory}
        onQuery={onQuery}
      />
    );
  }

  if (props.standalone) {
    return renderChartContainer();
  }

  return (
    <Styles id="explore-container" height={height}>
      <Global
        styles={css`
          .navbar {
            margin-bottom: 0;
          }
          body {
            max-height: 100vh;
            overflow: hidden;
          }
          #app-menu,
          #app {
            flex: 1 1 auto;
          }
          #app {
            flex-basis: 100%;
            overflow: hidden;
            height: 100vh;
          }
          #app-menu {
            flex-shrink: 0;
          }
        `}
      />
      {showingModal && (
        <SaveModal
          onHide={toggleModal}
          actions={props.actions}
          form_data={props.form_data}
          sliceName={props.sliceName}
          dashboardId={props.dashboardId}
        />
      )}
      <div
        className={
          isCollapsed ? 'no-show' : 'explore-column data-source-selection'
        }
      >
        <div className="title-container">
          <span className="horizont al-text">{t('Datasource')}</span>
          <span
            role="button"
            tabIndex={0}
            className="action-button"
            onClick={toggleCollapse}
          >
            <Icon
              name="expand"
              color={supersetTheme.colors.primary.base}
              className="collapse-icon"
              width={16}
            />
          </span>
        </div>
        <DataSourcePanel
          datasource={props.datasource}
          controls={props.controls}
          actions={props.actions}
        />
      </div>
      {isCollapsed ? (
        <div
          className="sidebar"
          onClick={toggleCollapse}
          data-test="open-datasource-tab"
          role="button"
          tabIndex={0}
        >
          <span role="button" tabIndex={0} className="action-button">
            <Tooltip title={t('Open Datasource Tab')}>
              <Icon
                name="collapse"
                color={supersetTheme.colors.primary.base}
                className="collapse-icon"
                width={16}
              />
            </Tooltip>
          </span>
          <Icon name="dataset-physical" width={16} />
        </div>
      ) : null}
      <div className="col-sm-3 explore-column controls-column">
        <QueryAndSaveBtns
          canAdd={!!(props.can_add || props.can_overwrite)}
          onQuery={onQuery}
          onSave={toggleModal}
          onStop={onStop}
          loading={props.chart.chartStatus === 'loading'}
          chartIsStale={chartIsStale}
          errorMessage={renderErrorMessage()}
          datasourceType={props.datasource_type}
        />
        <ConnectedControlPanelsContainer
          actions={props.actions}
          form_data={props.form_data}
          controls={props.controls}
          datasource_type={props.datasource_type}
          isDatasourceMetaLoading={props.isDatasourceMetaLoading}
        />
      </div>
      <div
        className={`main-explore-content ${
          isCollapsed ? 'col-sm-9' : 'col-sm-7'
        }`}
      >
        {renderChartContainer()}
      </div>
    </Styles>
  );
}

ExploreViewContainer.propTypes = propTypes;

function mapStateToProps(state) {
  const { explore, charts, impressionId } = state;
  const form_data = getFormDataFromControls(explore.controls);
  const chartKey = Object.keys(charts)[0];
  const chart = charts[chartKey];
  return {
    isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
    datasource: explore.datasource,
    datasource_type: explore.datasource.type,
    datasourceId: explore.datasource_id,
    dashboardId: explore.form_data ? explore.form_data.dashboardId : undefined,
    controls: explore.controls,
    can_overwrite: !!explore.can_overwrite,
    can_add: !!explore.can_add,
    can_download: !!explore.can_download,
    column_formats: explore.datasource
      ? explore.datasource.column_formats
      : null,
    containerId: explore.slice
      ? `slice-container-${explore.slice.slice_id}`
      : 'slice-container',
    isStarred: explore.isStarred,
    slice: explore.slice,
    sliceName: explore.sliceName,
    triggerRender: explore.triggerRender,
    form_data,
    table_name: form_data.datasource_name,
    vizType: form_data.viz_type,
    standalone: explore.standalone,
    forcedHeight: explore.forced_height,
    chart,
    timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
    impressionId,
  };
}

function mapDispatchToProps(dispatch) {
  const actions = {
    ...exploreActions,
    ...saveModalActions,
    ...chartActions,
    ...logActions,
  };
  return {
    actions: bindActionCreators(actions, dispatch),
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(ExploreViewContainer);
