import * as React from "react";
import { Input as BSInput, InputProps } from "reactstrap";
import { isArray, isFunction } from "util";
import { MathInput } from "./MathInput";
import { IInternalPromptProps } from "./Prompt";
import { IInputProps, InputPromptData } from "./QuizData";

const INDEX = 0;

export enum InputType {
  String = "string",
  Number = "number",
  Math = "math",
  Custom = "custom"
}

export type IInputAnswerHandler = (input: string) => boolean;
export type IInputAnswer =
  | string
  | number
  | string[]
  | number[]
  | IInputAnswerHandler;

export interface IInputPromptProps {
  children: (
    Input: React.ComponentType<IInputProps>
  ) => React.ReactChild | React.ReactChild[];
}

export class InputPrompt extends React.PureComponent<IInputPromptProps> {
  static is(value: any): value is InputPrompt {
    return React.isValidElement(value) && value.type === InputPrompt;
  }

  render() {
    return null;
  }
}

export interface IInternalInputPromptProps extends IInternalPromptProps {
  prompt: InputPromptData;
  input: string[];
  update(fn: (input: string[]) => string[]): string[];
}

interface IInputState {
  value: string;
}

export class InternalInputPrompt extends React.PureComponent<
  IInternalInputPromptProps
> {
  private static INPUT_STYLE = { width: "auto", display: "inline-block" };
  private Input: React.ComponentType<IInputProps>;
  private index: number = 0;

  constructor(props: IInternalInputPromptProps) {
    super(props);

    this.Input = this.createInput();
  }
  createOnChange = (index: number) => (e: any) => {
    this.props.update(input => {
      const nextInput = input.slice();
      nextInput[index] = e.target.value;
      return nextInput;
    });
  };
  componentDidUpdate(props: IInternalInputPromptProps) {
    if (props.prompt !== this.props.prompt) {
      this.index = 0;
    }
  }
  render() {
    return this.props.prompt.render(this.Input);
  }
  private createInput() {
    const _this = this;

    return class Input extends React.Component<IInputProps, IInputState> {
      static defaultProps = {
        Component: BSInput
      };

      private index: number;
      private Component: React.ComponentType<InputProps>;

      constructor(props: IInputProps) {
        super(props);

        this.index = _this.index++;
        this.state = { value: _this.props.input[this.index] || "" };
        this.Component =
          this.props.type === InputType.Math
            ? MathInput
            : (this.props.Component as any);

        _this.props.prompt.addAnswer(this.index, this.parseAnswer());
      }
      shouldComponentUpdate(props: IInputProps) {
        return this.state.value !== _this.props.input[this.index];
      }
      componentWillReceiveProps(props: IInputProps) {
        this.setState({ value: _this.props.input[this.index] || "" });
      }
      parseAnswer(): IInputAnswerHandler {
        switch (this.props.type) {
          case InputType.String:
            return createAnswer(this.props.answer, createStringAnswer);
          case InputType.Number:
            return createAnswer(this.props.answer, createNumberAnswer);
          case InputType.Math:
            return createAnswer(this.props.answer, createMathAnswer);
          case InputType.Custom:
            if (isFunction(this.props.answer)) {
              return this.props.answer as IInputAnswerHandler;
            } else {
              throw new TypeError("Custom type inputs must be functions");
            }
        }
      }
      render() {
        const Component = this.Component as any;

        return (
          <Component
            {...this.props.inputProps}
            disabled={_this.props.disabled}
            onChange={_this.createOnChange(this.index)}
            value={this.state.value}
            style={InternalInputPrompt.INPUT_STYLE}
          />
        );
      }
    };
  }
}

const createAnswer = (
  answer: any,
  createFn: (input: any) => IInputAnswerHandler
) => {
  if (isArray(answer)) {
    const answers = answer.map(a => createFn(a));

    return (input: any) => answers.some(fn => fn(input));
  } else {
    return createFn(answer);
  }
};

const createStringAnswer = (answer: string) => (input: string) => {
  return input === answer;
};

const createNumberAnswer = (answer: number) => (input: string) => {
  return parseFloat(input) === answer;
};

const createMathAnswer = (answer: string) => (input: string) => {
  return input === answer;
};
