// @flow
import * as React from 'react';
import {
	Input,
	Alert,
	InputGroup,
	InputGroupAddon,
	UncontrolledTooltip,
	Tooltip,
} from 'reactstrap';

import { rxField } from '@helpers/beat';

type Props = {
	// consumer-supplied func for validating input
	// if the func finds the input isnt valid, it should return a react.node of text for the error message
	validateFunc?: (string) => (true | React.Node),
	// if the current input in the CacheInput instance is valid, this func will be called with that value
	onChange: (any) => any,
	// the current value for this cacheinput
	value: string,
	// for edge cases
	children?: React.Node,
	// many fields in the beat editor are restricted to a certain "slugified" format
	// if this cacheinput needs that, use this flag
	fieldTest?: boolean,
	// update delay in ms, defaults to 500
	// basically just simple throttling to keep from overwhelming complex updates in the parent
	delay?: number,
	// danger color highlight
	invalid?: boolean,
};

type State = {
	// the current string in the CacheInput input field,
	// which may or may not be valid
	cache: string,
	// whether the cache is valid
	valid: boolean,
	// a text-like react node that represents an error message
	invalidityText?: React.Node,
	// the delay to use (default 500)
	msDelay: number,
};

// test for the "fieldTest" flag, with a nice error message
const fieldTestResults = (input: string): true | React.Node => {
	return (rxField.test(input) ||
		<span>
			The field name must be at least one character and consisting of only of alphanumeric ASCII characters, `_`, and `-`.
		</span>
	);
};

// top-level validity func with some edgecase checking
// validateFunc arg lets you also check using a user-defined validate func
const isValid = (input: string, currVal: string, validateFunc: (string) => true | React.Node, fieldTest: boolean): true | React.Node => {
	if(input === currVal)
		return true;

	if(fieldTest){
		const res = fieldTestResults(input);
		if(res !== true)
			return res;
	}

	const funcRes = validateFunc(input);

	return funcRes;
};

// Provides a bootstrap-styled wrapper around <input /> that caches the raw user input until a valid input is provided.
// Also throttles the onChange event to prevent many parent state updates in a short time
class CacheInput extends React.Component<Props, State>{
	constructor(props: Props){
		super(props);
	}

	state = {
		cache: this.props.value,
		valid: false,
		invalidityText: undefined,
		// default of 500ms delay
		msDelay: this.props.delay !== undefined ? this.props.delay : 500,
	};

	// check validation as soon as it mounts
	componentDidMount () {
		this.updateValidity(this.state.cache);
	}

	// if the parent's model (the props) changes, reset input
	componentDidUpdate = (prevProps: Props) => {
		if(this.props.value !== prevProps.value){
			this.setState({
				cache: (this.props.value),
			});
			this.clearTimer();
		}
	}

	// gets the validity info of an input string given a curr val
	updateValidity = (input: string, currVal?: string = this.props.value) => {
		const func = this.props.validateFunc || (() => true);
		const validRes = isValid(input, currVal, func, this.props.fieldTest || false);
		this.setState({
			valid: validRes === true,
			invalidityText: validRes !== true ? validRes : undefined
		});

		return validRes === true;
	}

	// the user can pass any props they want onto the inner <Input /> element
	// some props shouldn't be passed though
	getInputProps = () => {
		const inputProps = { ...this.props };
		delete inputProps['validateFunc'];
		delete inputProps['children'];
		delete inputProps['fieldTest'];
		delete inputProps['delay'];
		return inputProps;
	}

	// throttle timer ref
	onChangeTimer = null;
	// clears the throttle timer
	clearTimer = () => {
		if(this.onChangeTimer)
			clearTimeout(this.onChangeTimer);
	}

	// called whenever the user changes the raw input with the raw input event
	// for perf reasons it really focuses on returning when possible
	change = (e: any) => {
		this.clearTimer();
		const newInput = e.target.value;
		// no need to update the state if the value is the same
		if(newInput === this.state.cache)
			return;
		const newValid = this.updateValidity(newInput);
		this.setState({
			cache: newInput,
		});
		// no need to update props if value is the same
		if(newInput === this.props.value)
			return;
		if(newValid && this.props.onChange){
			if(this.state.msDelay === 0){
				this.props.onChange(e);
			} else {
				// by default in react events have a "persist" method
				// that lets you keep it around for a bit longer than normal
				// in testing environments the event persists by default
				if(e.persist)
					e.persist();
				this.onChangeTimer = setTimeout(() => {
					this.props.onChange(e);
				}, this.state.msDelay);
			}
		}
	}

	// the Tooltips are annoying in test environments and need a react ref
	target = null

	render () {
		// either if invalid or if the consuming component set it
		const dangerColor = !this.state.valid || this.props.invalid;
		return (
			<InputGroup>
				{ this.props.children }
				<Input
					type='text'
					{...this.getInputProps()}
					value={this.state.cache}
					onChange={this.change}
					valid={!dangerColor}
					invalid={dangerColor}
				/>
				{ !this.state.valid &&
						<div ref={target => {this.target = target;}} className='input-group-append'>
							<span className='input-group-text'>
								<strong className='text-danger'>!</strong>
							</span>
						</div>
				}
				{ !this.state.valid &&
						<UncontrolledTooltip target={() => this.target}>
							{ this.state.invalidityText }
						</UncontrolledTooltip>
				}
			</InputGroup>
		);
	}
}

export default CacheInput;
