/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */

/* eslint-disable react/prefer-es6-class, jsx-a11y/no-noninteractive-element-interactions */

// Implements the [Modal design pattern](https://lightningdesignsystem.com/components/modals/) in React.
// Based on SLDS v2.2.1

import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';

import classNames from 'classnames';
import ReactModal from 'react-modal';

// ### shortid
// [npmjs.com/package/shortid](https://www.npmjs.com/package/shortid)
// shortid is a short, non-sequential, url-friendly, unique id generator
import shortid from 'shortid';

// This component's `checkProps` which issues warnings to developers about properties when in development mode (similar to React's built in development tools)
import checkProps from './check-props';

import checkAppElementIsSet from '../../utilities/warning/check-app-element-set';

import Button from '../button';

import { MODAL } from '../../utilities/constants';
import componentDoc from './docs.json';

const documentDefined = typeof document !== 'undefined';
const windowDefined = typeof window !== 'undefined';

const propTypes = {
	/**
	 * Vertical alignment of Modal.
	 */
	align: PropTypes.oneOf(['top', 'center']),
	/**
	 * Boolean indicating if the appElement should be hidden.
	 */
	ariaHideApp: PropTypes.bool,
	/**
	 * **Assistive text for accessibility.**
	 * This object is merged with the default props object on every render.
	 * * `dialogLabel`: This is a visually hidden label for the dialog. If not provided, `title` is used.
	 * * `closeButton`: This is a visually hidden label for the close button.
	 */
	assistiveText: PropTypes.shape({
		dialogLabel: PropTypes.string,
		closeButton: PropTypes.string,
	}),
	/**
	 * Modal content.
	 */
	children: PropTypes.node.isRequired,
	/**
	 * Custom CSS classes for the modal's container. This is the element with `.slds-modal__container`. Use `classNames` [API](https://github.com/JedWatson/classnames).
	 */
	containerClassName: PropTypes.oneOfType([
		PropTypes.array,
		PropTypes.object,
		PropTypes.string,
	]),
	/**
	 * Custom CSS classes for the modal's body. This is the element that has overflow rules and should be used to set a static height if desired. Use `classNames` [API](https://github.com/JedWatson/classnames).
	 */
	contentClassName: PropTypes.oneOfType([
		PropTypes.array,
		PropTypes.object,
		PropTypes.string,
	]),
	/**
	 * Custom styles for the modal's body. This is the element that has overflow rules and should be used to set a static height if desired.
	 */
	contentStyle: PropTypes.object,
	/**
	 * If true, modal footer buttons render left and right. An example use case would be for "back" and "next" buttons.
	 */
	directional: PropTypes.bool,
	/**
	 * If true, Modals can be dismissed by clicking on the close icon or pressing esc key.
	 */
	dismissible: PropTypes.bool,
	/**
	 * If true, Modals can be dismissed by clicking outside of modal. If unspecified, defaults to dismissible.
	 */
	dismissOnClickOutside: PropTypes.bool,
	/**
	 * Callback to fire with Modal is dismissed
	 */
	onRequestClose: PropTypes.func,
	/**
	 * Accepts a node or array of nodes that are typically a `Button` or `ProgressIndicator`. If an array, the nodes render on the right side first but are then floated left and right if <code>directional</code> prop is `true`.
	 */
	footer: PropTypes.oneOfType([PropTypes.array, PropTypes.node]),
	/**
	 * Allows for a custom modal header that does not scroll with modal content. If this is defined, `title` and `tagline` will be ignored. The close button will still be present.
	 */
	header: PropTypes.node,
	/**
	 * Adds CSS classes to the container surrounding the modal header and the close button. Use `classNames` [API](https://github.com/JedWatson/classnames).
	 */
	headerClassName: PropTypes.oneOfType([
		PropTypes.array,
		PropTypes.object,
		PropTypes.string,
	]),
	/**
	 * Forces the modal to be open or closed.
	 */
	isOpen: PropTypes.bool.isRequired,
	/**
	 * Function whose return value is the mount node to insert the Modal element into. The default is `() => document.body`.
	 */
	parentSelector: PropTypes.func,
	/**
	 * Custom CSS classes for the portal DOM node. This node is a direct descendant of the `body` and is the parent of `ReactModal__Overlay`. Use `classNames` [API](https://github.com/JedWatson/classnames).
	 */
	portalClassName: PropTypes.oneOfType([
		PropTypes.array,
		PropTypes.object,
		PropTypes.string,
	]),
	/**
	 * Styles the modal as a prompt.
	 */
	prompt: PropTypes.oneOf([
		'success',
		'warning',
		'error',
		'wrench',
		'offline',
		'info',
	]),
	/**
	 * Specifiies the modal's width. May be deprecated in favor of `width` in the future.
	 */
	size: PropTypes.oneOf(['medium', 'large']),
	/**
	 * Content underneath the title in the modal header.
	 */
	tagline: PropTypes.node,
	/**
	 * Text heading at the top of a modal.
	 */
	title: PropTypes.node,
	/**
	 * Allows adding additional notifications within the modal.
	 */
	toast: PropTypes.node,
};

const defaultProps = {
	assistiveText: {
		dialogLabel: '',
		closeButton: 'Close',
	},
	align: 'center',
	ariaHideApp: true,
	dismissible: true,
};

/**
 * The Modal component is used for the Lightning Design System Modal and Notification > Prompt components. The Modal opens from a state change outside of the component itself (pass this state to the <code>isOpen</code> prop). For more details on the Prompt markup, please review the <a href="http://www.lightningdesignsystem.com/components/notifications#prompt">Notifications > Prompt</a>.
 *
 * By default, `Modal` will add `aria-hidden=true` to the `body` tag, but this disables some assistive technologies. To prevent this you can add the following to your application with `#mount` being the root node of your application that you would like to hide from assistive technologies when the `Modal` is open.
 * ```
 * import settings from 'design-system-react/components/settings';
 * settings.setAppElement('#mount');
 * ```
 * This component uses a portalMount (a disconnected React subtree mount) to create a modal as a child of `body`.
 */
class Modal extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			isClosing: false,
		};

		// Bind
		this.handleModalClick = this.handleModalClick.bind(this);
		this.closeModal = this.closeModal.bind(this);
		this.dismissModalOnClickOutside = this.dismissModalOnClickOutside.bind(
			this
		);
	}

	componentWillMount() {
		this.generatedId = shortid.generate();
		checkProps(MODAL, this.props, componentDoc);
		if (this.props.ariaHideApp) {
			checkAppElementIsSet();
		}
	}

	componentDidMount() {
		this.setReturnFocus();
		this.updateBodyScroll();
	}

	componentDidUpdate(prevProps, prevState) {
		if (this.props.isOpen !== prevProps.isOpen) {
			this.updateBodyScroll();
		}
		if (this.state.isClosing !== prevState.isClosing) {
			if (this.state.isClosing) {
				// This section of code should be removed once trigger.jsx
				// and manager.jsx are removed. They appear to have
				// been created in order to do modals in portals.
				if (!this.isUnmounting) {
					const el = ReactDOM.findDOMNode(this); // eslint-disable-line react/no-find-dom-node
					if (
						el &&
						el.parentNode &&
						el.parentNode.getAttribute('data-slds-modal')
					) {
						ReactDOM.unmountComponentAtNode(el);
						document.body.removeChild(el);
					}
				}
			}
		}
	}

	componentWillUnmount() {
		this.isUnmounting = true;
		this.clearBodyScroll();
	}

	getId() {
		return this.props.id || this.generatedId;
	}

	getModal() {
		const modalStyle =
			this.props.align === 'top' ? { justifyContent: 'flex-start' } : null;
		const borderRadius =
			this.props.title || this.props.header ? {} : { borderRadius: '.25rem' };
		const contentStyleFromProps = this.props.contentStyle || {};
		const contentStyle = {
			...borderRadius,
			...contentStyleFromProps,
		};
		return (
			// temporarily disabling eslint for the onClicks on the div tags
			/* eslint-disable */
			<div
				aria-label={this.props.assistiveText.dialogLabel}
				aria-labelledby={
					!this.props.assistiveText.dialogLabel && this.props.title
						? this.getId()
						: null
				}
				className={classNames({
					'slds-modal': true,
					'slds-fade-in-open': true,
					'slds-modal_large': this.props.size === 'large',
					'slds-modal_prompt': this.isPrompt(),
				})}
				onClick={this.dismissModalOnClickOutside}
				role={this.props.dismissible ? 'dialog' : 'alertdialog'}
			>
				<div
					className={classNames(
						'slds-modal__container',
						this.props.containerClassName
					)}
					style={modalStyle}
				>
					{this.headerComponent()}
					<div
						className={classNames(
							'slds-modal__content',
							this.props.contentClassName
						)}
						style={contentStyle}
						onClick={this.handleModalClick}
					>
						{this.props.children}
					</div>
					{this.footerComponent()}
				</div>
			</div>
			/* eslint-enable */
		);
	}

	setReturnFocus() {
		this.setState({
			returnFocusTo: documentDefined ? document.activeElement : null,
		});
	}

	// eslint-disable-next-line class-methods-use-this
	clearBodyScroll() {
		if (windowDefined && documentDefined && document.body) {
			document.body.style.overflow = 'inherit';
		}
	}

	closeModal() {
		if (this.props.dismissible) {
			this.dismissModal();
		}
	}

	dismissModal() {
		this.setState({ isClosing: true });
		if (this.state.returnFocusTo && this.state.returnFocusTo.focus) {
			this.state.returnFocusTo.focus();
		}
		if (this.props.onRequestClose) {
			this.props.onRequestClose();
		}
	}

	dismissModalOnClickOutside() {
		// if dismissOnClickOutside is not set, default its value to dismissible
		const dismissOnClickOutside = this.props.dismissOnClickOutside
			? this.props.dismissOnClickOutside
			: this.props.dismissible;

		if (dismissOnClickOutside) {
			this.dismissModal();
		}
	}

	footerComponent() {
		let footer = null;
		const hasFooter = this.props.footer;
		const footerClass = {
			'slds-modal__footer': true,
			'slds-modal__footer_directional': this.props.directional,
			'slds-theme_default': this.isPrompt(),
		};

		if (hasFooter) {
			footer = ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/no-noninteractive-element-interactions
				<footer
					className={classNames(footerClass, this.props.footerClassNames)}
					onClick={this.handleModalClick}
				>
					{this.props.footer}
				</footer>
			);
		}
		return footer;
	}

	// eslint-disable-next-line class-methods-use-this
	handleModalClick(event) {
		if (event && event.stopPropagation) {
			event.stopPropagation();
		}
	}

	handleSubmitModal() {
		this.closeModal();
	}

	headerComponent() {
		let headerContent = this.props.header;
		const headerEmpty =
			!headerContent && !this.props.title && !this.props.tagline;
		const assistiveText = {
			...defaultProps.assistiveText,
			...this.props.assistiveText,
		};
		const closeButtonAssistiveText =
			this.props.closeButtonAssistiveText || assistiveText.closeButton;
		const closeButton = (
			<Button
				assistiveText={{ icon: closeButtonAssistiveText }}
				iconCategory="utility"
				iconName="close"
				iconSize="large"
				inverse
				className="slds-modal__close"
				onClick={this.closeModal}
				title={closeButtonAssistiveText}
				variant="icon"
			/>
		);

		if ((!headerContent && this.props.title) || this.props.tagline) {
			headerContent = (
				<div>
					{this.props.toast}
					<h2
						className={classNames({
							'slds-text-heading_small': this.isPrompt(),
							'slds-text-heading_medium': !this.isPrompt(),
						})}
						id={this.getId()}
					>
						{this.props.title}
					</h2>
					{this.props.tagline ? (
						<p className="slds-m-top_x-small">{this.props.tagline}</p>
					) : null}
				</div>
			);
		}

		return (
			// eslint-disable-next-line jsx-a11y/no-static-element-interactions
			<header
				className={classNames(
					'slds-modal__header',
					{
						'slds-modal__header_empty': headerEmpty,
						[`slds-theme_${this.props.prompt}`]: this.isPrompt(),
						'slds-theme_alert-texture': this.isPrompt(),
					},
					this.props.headerClassName
				)}
				onClick={this.handleModalClick}
			>
				{this.props.dismissible ? closeButton : null}
				{headerContent}
			</header>
		);
	}

	isPrompt() {
		return this.props.prompt !== undefined;
	}

	updateBodyScroll() {
		if (windowDefined && documentDefined && document.body) {
			if (this.props.isOpen) {
				document.body.style.overflow = 'hidden';
			} else {
				document.body.style.overflow = 'inherit';
			}
		}
	}

	render() {
		const customStyles = {
			content: {
				position: 'default',
				top: 'default',
				left: 'default',
				right: 'default',
				bottom: 'default',
				border: 'default',
				background: 'default',
				overflow: 'default',
				WebkitOverflowScrolling: 'default',
				borderRadius: 'default',
				outline: 'default',
				padding: 'default',
			},
			overlay: {
				zIndex: 8000, // following SLDS guideline for z-index overlay
				backgroundColor: 'default',
			},
		};

		return (
			<ReactModal
				ariaHideApp={this.props.ariaHideApp}
				contentLabel="Modal"
				isOpen={this.props.isOpen}
				onRequestClose={this.closeModal}
				style={customStyles}
				parentSelector={this.props.parentSelector}
				portalClassName={classNames(
					'ReactModalPortal',
					this.props.portalClassName
				)}
			>
				{this.getModal()}
				<div className="slds-backdrop slds-backdrop_open" />
			</ReactModal>
		);
	}
}

Modal.displayName = MODAL;
Modal.propTypes = propTypes;
Modal.defaultProps = defaultProps;

export default Modal;
