// Imports
const { Container, Header, Button, Form, Icon, Popup, Card, Message, Loader } = semanticUIReact;

class App extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            datasets: [],
            argoWorkflowsUrl: '',
            messages: [],
            error: undefined,
        };
    }

    fetchConfig() {
        const setConfig = ({datasets, argoWorkflowsUrl}) => this.setState({
            datasets,
            argoWorkflowsUrl,
            error: undefined,
        });
        const setError = (error) => this.setState({ error: error });
        axios.get('/config').then(config => setConfig(config.data), setError);
    }

    componentDidMount() {
        this.fetchConfig();
    }

    onEvent = (identifier, message, data, type, workflowId) => {
        const { messages, argoWorkflowsUrl } = this.state;
        const links = [];
        if (workflowId) {
            links.push({
                url: `${argoWorkflowsUrl}/${workflowId}`,
                text: workflowId,
            });
        }
        this.setState({
            messages: messages.concat({
                time: Date.now(),
                identifier,
                message,
                data,
                type,
                links,
            }),
        });
    };

    renderCards() {
        const { datasets } = this.state;
        return datasets
            .filter(dataset => dataset.hide !== true)
            .map((set, i) => <DataSet key={i} identifier={set.identifier} type={set.type} documentation={set.documentation}
                         upload={set.upload} run={set.run}
                         onEvent={this.onEvent} />
            );
    }

    render() {
        const { config, error, messages } = this.state;
        const initError = error !== undefined;

        return (
            <Container>
                <h2>Data Sets</h2>
                <Card.Group>
                    {this.renderCards()}
                </Card.Group>
                <EventLogArea messages={messages}  />
            </Container>
        );
    }
}

class DataSet extends React.PureComponent {

    constructor(props) {
        super(props);
        this.state = {
            identifier: props.identifier,
            type: props.type,
            documentation: props.documentation,
            upload: props.upload,
            run: props.run,
            inProgress: false,
            uploadInProgress: false,
        };
    }

    onEvent(message, data, type, workflowId) {
        const onActionEvent = this.props.onEvent;
        onActionEvent(this.state.identifier, message, data, type, workflowId);
    }

    renderUpload() {
        const { identifier, upload } = this.state;
        const isUploadedble = upload && upload.enabled;
        const isVisible = upload && upload.hide !== true;
        const isVersionRequired = !upload.fileName;

        return isUploadedble && isVisible
          ? <UploadComponent identifier={identifier} encapsulate={upload.encapsulate} fileName={upload.fileName} isVersionRequired={isVersionRequired} parameters={upload.parameters} onEvent={this.onEvent.bind(this)} />
          : null;
    }

    renderRun() {
        const { identifier, run } = this.state;
        const isRunnable = run && run.enabled;
        const isVisible = run && run.hide !== true;
        return isRunnable && isVisible
          ? <RunComponent identifier={identifier} parameters={run.parameters} onEvent={this.onEvent.bind(this)} />
          : null;
    }

    render() {
        const { identifier, type, documentation } = this.state;
        return (
          <Card>
              <Card.Content>
                  <Card.Header>
                      {identifier}
                      {' '}
                      <Documentation documentation={documentation}/>
                  </Card.Header>
                  <Card.Meta>{type}</Card.Meta>
              </Card.Content>
              {this.renderUpload()}
              {this.renderRun()}
          </Card>
        );
    }
}

class RunComponent extends React.PureComponent {
    constructor(props) {
        super(props);
        const userInput = getInputDefaultValues(props.parameters);
        this.state = {
            identifier: props.identifier,
            parameters: props.parameters,
            inProgress: false,
            userInput,
            isValid: this.isValid(props.parameters, userInput),
        };
    }

    composeUrl(identifier, userInput) {
        const queryParameters = mapObjectToArray(userInput)
          .map(([key, val]) => `${key}=${val}`);

        return queryParameters.length > 0
          ? `/run/${identifier}?${queryParameters.join('&')}`
          : `/run/${identifier}`;
    }

    run() {
        const { identifier, parameters, userInput } = this.state;
        const isValid = this.isValid(parameters, userInput);
        console.log('isValid', isValid);
        if (!isValid) {
            this.setState({ isValid: false });
            this.props.onEvent('Validation Error.', null, TYPE_WARNING);
            return;
        }


        this.setState({ inProgress: true });
        const runSuccess = () => (response) => {
            this.props.onEvent('Run successfully completed', null, TYPE_SUCCESS, response.data);
            this.setState({ inProgress: false });
        };
        const runError = () => (error) => {
            this.props.onEvent('Run failed.', errorMap(error), TYPE_ERROR);
            this.setState({ inProgress: false });
        };
        axios.post(this.composeUrl(identifier, userInput)).then(runSuccess(), runError());
    }

    onChange(e, { name, value }) {
        const { userInput, parameters } = this.state;
        const userInputChange = Object.assign({}, userInput, { [name]: value } );
        const isValid = this.isValid(parameters, userInputChange);

        this.setState({ userInput: userInputChange, isValid });
    }

    hasError(userInput, param, isRequired) {
        return userInput[param] === undefined
          ? false
          : (isRequired && userInput[param].length === 0)
    }

    isValid(parameters, userInput) {
        const requiredParameters = mapObjectToArray(parameters)
          .filter(([ param, {required, defaultValue}]) => required)
          .map(([ param ]) => param);

        const filledParameters = mapObjectToArray(userInput)
          .filter(([param, value]) => value !== undefined && value.length > 0)
          .map(([param, value]) => param);

        return requiredParameters.reduce((isValid, parameter) => isValid && filledParameters.indexOf(parameter) !== -1 , true);
    }

    renderForm() {
        const { parameters, userInput } = this.state;
        if (!parameters) {
            return;
        }
        return (<Form>
            <h4>Run Parameters</h4>
            {mapObjectToArray(parameters)
                .filter(([param, {hide}]) => !hide)
                .map(([param, {required, defaultValue, documentation}], key) => (
                    <LabeledField key={key} param={param} value={this.state.userInput[param]} required={required}
                                  documentation={documentation}
                                  onChange={this.onChange.bind(this)} error={this.hasError(userInput, param, required)}/>
                ))
            }
        </Form>);
    }

    render() {
        const { isValid, inProgress } = this.state;
        return (
            <Card.Content extra>
                {this.renderForm()}
                <Button basic className="ui fluid button" color='green'
                        disabled={!isValid || inProgress} onClick={this.run.bind(this)}>
                    {inProgress ? <Loader size='tiny' active inline='centered' /> : 'Run'}
                </Button>
            </Card.Content>
        );
    }
}

class UploadComponent extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            identifier: props.identifier,
            encapsulate: props.encapsulate,
            isVersionRequired: props.isVersionRequired,
            parameters: this.fixParametersWithVersion(props.parameters),
            inProgress: false,
            userInput: getInputDefaultValues(props.parameters),
            selectedFile: undefined,
            isValid: false,
        };
    }

    fixParametersWithVersion(parameters) {
        if (this.props.isVersionRequired && (!parameters || parameters.version === undefined)) {
            return Object.assign({}, { version: {required: true} }, parameters);
        }
        return parameters;
    }

    composeUrl(identifier, userInput) {
        const { version } = userInput;
        const queryParameters = mapObjectToArray(userInput)
          .filter(([key, val]) => key !== 'version')
          .map(([key, val]) => `${key}=${val}`);

        let prefix = `/upload/${identifier}`;
        if (this.props.isVersionRequired) {
            prefix = `${prefix}/${version}`;
        }

        return queryParameters.length > 0
          ? `${prefix}?${queryParameters.join('&')}`
          : prefix;
    }

    upload() {
        const { identifier, userInput, selectedFile } = this.state;
        this.setState({ inProgress: true });
        const uploadSuccess = (response) => {
            this.props.onEvent('upload successfully completed', null, TYPE_SUCCESS, response.data);
            this.setState({ inProgress: false });
        };
        const uploadError = (error) => {
            this.props.onEvent('upload failed.', errorMap(error), TYPE_ERROR);
            this.setState({ inProgress: false });
        };

        axios.put(this.composeUrl(identifier, userInput), selectedFile, {
            headers: {
                "Content-Type": "application/octet-stream",
                "X-Upload-Filename": selectedFile.name
            }
        }).then(uploadSuccess, uploadError);
    }

    onChange(e, { name, value }) {
        const { userInput, parameters, selectedFile } = this.state;
        const userInputChange = Object.assign({}, userInput, { [name]: value } );
        const isValid = this.isValid(parameters, userInputChange, selectedFile);

        this.setState({ userInput: userInputChange, isValid });
    }

    onFileChange(e) {
        const { parameters, userInput } = this.state;
        const selectedFile = e.target.files.length > 0 ? e.target.files[0] : undefined;
        this.setState({ selectedFile, isValid: this.isValid(parameters, userInput, selectedFile) });
    }

    hasError(userInput, param, isRequired) {
        return userInput[param] === undefined
          ? false
          : (isRequired && userInput[param].length === 0)
    }

    isValid(parameters, userInput, selectedFile) {
        const requiredParameters = mapObjectToArray(parameters)
          .filter(([ param, {required, defaultValue} ]) => required)
          .map(([ param ]) => param);

        const filledParameters = mapObjectToArray(userInput)
          .filter(([param, value]) => value !== undefined && value.length > 0)
          .map(([param, value]) => param);

        return selectedFile !== undefined &&
          requiredParameters.reduce((isValid, parameter) => isValid && filledParameters.indexOf(parameter) !== -1 , true);
    }

    renderForm() {
        const { encapsulate, parameters, userInput } = this.state;
        if (!parameters) {
            return;
        }
        const filePlaceholder = this.props.fileName || encapsulate || '*.tar.gz';
        return (<Form>
            <h4>Upload Parameters <small>({filePlaceholder})</small></h4>
            <Form.Field key={-1}>
                <label style={{ color: '#B00' }}>File *</label>
                <input type="file" onChange={this.onFileChange.bind(this)} />
            </Form.Field>

            {mapObjectToArray(parameters)
                .filter(([param, {hide}]) => !hide)
                .map(([param, {required, defaultValue, documentation}], key) => (
                <LabeledField key={key} param={param} value={this.state.userInput[param]} required={required}
                              documentation={documentation}
                              onChange={this.onChange.bind(this)} error={this.hasError(userInput, param, required)}/>
            ))}
        </Form>);
    }

    render() {
        const { isValid, inProgress } = this.state;
        return (
          <Card.Content extra>
              {this.renderForm()}
              <Button basic className="ui fluid button" color='blue' disabled={!isValid || inProgress}
                      onClick={this.upload.bind(this)}>{inProgress ? <Loader size='tiny' active inline='centered' /> : 'Upload'}</Button>
          </Card.Content>
        );
    }
}

const errorMap = (error) => {
    if (error.response) {
        return `HTTP Status ${error.response.status} - ${error.response.statusText}, ${error.response.data}`;
    } else {
        return `No response - ${error.message}`;
    }
};

const TYPE_SUCCESS = 'success';
const TYPE_ERROR = 'error';
const TYPE_WARNING = 'warning';

function LabeledField({param, value, required, onChange, error, documentation}) {
    const documentationIcon = <Documentation documentation={documentation}/>;

    return (
        <Form.Field>
            {required ? (
                <label style={{color: '#B00'}}>{param} * {documentationIcon}</label>
            ) : (
                <label>{param} {documentationIcon}</label>
            )}
            <Form.Input className="fluid" name={param} placeholder={param} required={required} value={value}
                        onChange={onChange} error={error}/>
        </Form.Field>
    );
}

function Documentation({documentation}) {
    if(!documentation){
        return null;
    }
    return (
        <Popup on="click" flowing trigger={<Icon name="info" link/>} content={<ReactMarkdown source={documentation}/>}/>
    );
}

class EventLogArea extends React.PureComponent {
    render() {
        const { messages } = this.props;

        return (
          <>
              <Header as='h1'>Event Log</Header>
              <div>
                    {messages.map((m, i) => <EventMessage event={m} key={i} />).reverse()}
              </div>
          </>
        );
    }
}

const formatEvent = (event) => {
    return event.data
      ? `${new Date(event.time).toLocaleString()}: ${event.message} - "${event.data}"`
      : `${new Date(event.time).toLocaleString()}: ${event.message}`;
};

const EventMessage = ({event}) => (
    <Message negative={event.type === 'error'}
             success={event.type === 'success'}
             warning={event.type === 'warning'}
             header={event.identifier}
             content={
                 <>
                     {formatEvent(event)}
                     {event.links.length > 0 && (
                         <>
                             {' - '}
                             {event.links.map(link => (
                                 <a href={link.url} target="_blank">{link.text}</a>
                             ))}
                         </>
                     )}
                 </>
             }/>
);

ReactDOM.render(<App />, document.getElementById('container'));

/**
 * Convert Object to key value pair array.
 * { name: 'Sample', age: 29 } => [ ['name', 'Sample'], ['age', 29] ]
 * @param obj
 */
function mapObjectToArray(obj) {
    if (obj === undefined || typeof obj !== 'object') {
        return [];
    }
    const result = [];
    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
            result.push([key, obj[key]]);
        }
    }
    return result;
}

function getInputDefaultValues(parameters) {
    const userInput = {};
    Object.keys(parameters).forEach(key => userInput[key] = parameters[key].defaultValue);
    return userInput;
}
