import React, { Component } from 'react';
import { Editor } from 'eu-eleysion-recogito-client-core';
import Highlighter from './highlighter/Highlighter';
import SelectionHandler from './selection/SelectionHandler';
import RelationsLayer from './relations/RelationsLayer';
import RelationEditor from './relations/editor/RelationEditor';

import './TextAnnotator.scss';

/**
 * Pulls the strings between the annotation highlight layer
 * and the editor popup.
 */
export default class TextAnnotator extends Component {
    constructor(props) {
        super(props);

        this.state = {
            selectedAnnotation: null,
            selectedDOMElement: null,
            selectedRelation: null,

            // ReadOnly mode
            readOnly: this.props.config.readOnly,

            widgets: this.props.config.widgets,

            // Headless mode
            editorDisabled: this.props.config.disableEditor,
        };

        this._editor = React.createRef();
    }

    /** Shorthand **/
    clearState = () => {
        this.setState({
            selectedAnnotation: null,
            selectedDOMElement: null,
        });

        this.selectionHandler.enabled = true;
    };

    handleEscape = (evt) => {
        if (evt.which === 27) this.onCancelAnnotation();
    };

    componentDidMount() {
        this.highlighter = new Highlighter(
            this.props.contentEl,
            this.props.config.formatter
        );

        this.selectionHandler = new SelectionHandler(
            this.props.contentEl,
            this.highlighter,
            this.props.config.readOnly
        );
        this.selectionHandler.on('select', this.handleSelect);

        this.relationsLayer = new RelationsLayer(this.props.contentEl);

        this.relationsLayer.on('createRelation', this.onEditRelation);
        this.relationsLayer.on('selectRelation', this.onEditRelation);
        this.relationsLayer.on('cancelDrawing', this.closeRelationsEditor);

        document.addEventListener('keydown', this.handleEscape);
    }

    componentWillUnmount() {
        document.removeEventListener('keydown', this.handleEscape);
    }

    onChanged = () => {
        // Disable selection outside of the editor
        // when user makes the first change
        this.selectionHandler.enabled = false;
    };

    /**************************/
    /* Annotation CRUD events */
    /**************************/

    /** Selection on the text **/
    handleSelect = (evt) => {
        this.state.editorDisabled
            ? this.onHeadlessSelect(evt)
            : this.onNormalSelect(evt);
    };

    onNormalSelect = (evt) => {
        const { selection, element } = evt;
        if (selection) {
            this.setState(
                {
                    selectedAnnotation: null,
                    selectedDOMElement: null,
                },
                () =>
                    this.setState({
                        selectedAnnotation: selection,
                        selectedDOMElement: element,
                    })
            );

            if (!selection.isSelection)
                this.props.onAnnotationSelected(selection.clone(), element);
        } else {
            this.clearState();
        }
    };

    onHeadlessSelect = (evt) => {
        const { selection, element } = evt;
        if (selection) {
            this.setState(
                {
                    selectedAnnotation: null,
                    selectedDOMElement: null,
                },
                () =>
                    this.setState({
                        selectedAnnotation: selection,
                        selectedDOMElement: element,
                    })
            );

            if (!selection.isSelection) {
                // Selection of existing annotation
                this.props.onAnnotationSelected(selection.clone(), element);
            } else {
                // Notify backend text selection to create a new annotation
                const undraft = (annotation) =>
                    annotation.clone({
                        body: annotation.bodies.map(
                            ({ draft, ...rest }) => rest
                        ),
                    });
                this.onCreateOrUpdateAnnotation('onAnnotationCreated')(
                    undraft(selection).toAnnotation()
                );
            }
        } else {
            this.clearState();
        }
    };

    /**
     * A convenience method that allows the external application to
     * override the autogenerated Id for an annotation.
     *
     * Usually, the override will happen almost immediately after
     * the annotation is created. But we need to be defensive and assume
     * that the override might come in with considerable delay, thus
     * the user might have made further edits already.
     *
     * A key challenge here is that there may be dependencies between
     * the original annotation and relations that were created meanwhile.
     */
    overrideAnnotationId = (originalAnnotation) => (forcedId) => {
        const { id } = originalAnnotation;

        // After the annotation update, we need to update dependencies
        // on the annotation layer, if any
        const updateDependentRelations = (updatedAnnotation) => {
            // Wait until the highlighter update has come into effect
            requestAnimationFrame(() => {
                this.relationsLayer.overrideTargetAnnotation(
                    originalAnnotation,
                    updatedAnnotation
                );
            });
        };

        // Force the editors to close first, otherwise their annotations will be orphaned
        if (this.state.selectedAnnotation || this.state.selectedRelation) {
            this.relationsLayer.resetDrawing();
            this.setState(
                {
                    selectedAnnotation: null,
                    selectedRelation: null,
                },
                () => {
                    const updated = this.highlighter.overrideId(id, forcedId);
                    updateDependentRelations(updated);
                }
            );
        } else {
            const updated = this.highlighter.overrideId(id, forcedId);
            updateDependentRelations(updated);
        }
    };

    /**
     * A convenience method that allows the external application to
     * override the autogenerated Id for a relation.
     *
     * This operation is less problematic than .overrideAnnotation().
     * We just need to make sure the RelationEditor is closed, so that
     * the annotation doesn't become orphaned. Otherwise, there are
     * no dependencies.
     */
    overrideRelationId = (originalId) => (forcedId) => {
        if (this.state.selectedRelation) {
            this.setState({ selectedRelation: null }, () =>
                this.relationsLayer.overrideRelationId(originalId, forcedId)
            );
        } else {
            this.relationsLayer.overrideRelationId(originalId, forcedId);
        }
    };

    /** Common handler for annotation CREATE or UPDATE **/
    onCreateOrUpdateAnnotation = (method) => (annotation, previous) => {
        this.clearState();

        this.selectionHandler.clearSelection();
        this.highlighter.addOrUpdateAnnotation(annotation, previous);

        // Call CREATE or UPDATE handler
        if (previous) this.props[method](annotation.clone(), previous.clone());
        else
            this.props[method](
                annotation.clone(),
                this.overrideAnnotationId(annotation)
            );
    };

    onDeleteAnnotation = (annotation) => {
        // Delete connections
        this.relationsLayer.destroyConnectionsFor(annotation);

        this.clearState();
        this.selectionHandler.clearSelection();
        this.highlighter.removeAnnotation(annotation);

        this.props.onAnnotationDeleted(annotation);
    };

    /** Cancel button on annotation editor **/
    onCancelAnnotation = (annotation) => {
        this.clearState();
        this.selectionHandler.clearSelection();
        if (annotation) this.props.onCancelSelected(annotation);
    };

    /************************/
    /* Relation CRUD events */
    /************************/

    // Shorthand
    closeRelationsEditor = () => {
        this.setState({ selectedRelation: null });
        this.relationsLayer.resetDrawing();
    };

    /**
     * Selection on the relations layer: open an existing
     * or newly created connection for editing.
     */
    onEditRelation = (relation) => {
        this.setState({
            selectedRelation: relation,
        });
    };

    /** 'Ok' on the relation editor popup **/
    onCreateOrUpdateRelation = (relation, previous) => {
        this.relationsLayer.addOrUpdateRelation(relation, previous);
        this.closeRelationsEditor();

        // This method will always receive a 'previous' connection -
        // if the previous is just an empty connection, fire 'create',
        // otherwise, fire 'update'
        const isNew = previous.annotation.bodies.length === 0;

        if (isNew)
            this.props.onAnnotationCreated(
                relation.annotation.clone(),
                this.overrideRelationId(relation.annotation.id)
            );
        else
            this.props.onAnnotationUpdated(
                relation.annotation.clone(),
                previous.annotation.clone()
            );
    };

    /** 'Delete' on the relation editor popup **/
    onDeleteRelation = (relation) => {
        this.relationsLayer.removeRelation(relation);
        this.closeRelationsEditor();
        this.props.onAnnotationDeleted(relation.annotation);
    };

    /****************/
    /* External API */
    /****************/

    addAnnotation = (annotation) => {
        this.highlighter.addOrUpdateAnnotation(annotation.clone());
    };

    get disableSelect() {
        return !this.selectionHandler.enabled;
    }

    set disableSelect(disable) {
        if (disable) this.props.contentEl.classList.add('r6o-noselect');
        else this.props.contentEl.classList.remove('r6o-noselect');

        this.selectionHandler.enabled = !disable;
    }

    getAnnotations = () => {
        const annotations = this.highlighter.getAllAnnotations();
        const relations = this.relationsLayer.getAllRelations();
        return annotations.concat(relations).map((a) => a.clone());
    };

    removeAnnotation = (annotation) => {
        this.highlighter.removeAnnotation(annotation);

        // If the editor is currently open on this annotation, close it
        const { selectedAnnotation } = this.state;
        if (selectedAnnotation && annotation.isEqual(selectedAnnotation))
            this.clearState();
    };

    selectAnnotation = (arg) => {
        // De-select in any case
        this.setState(
            {
                selectedAnnotation: null,
                selectedDOMElement: null,
            },
            () => {
                if (arg) {
                    const spans = this.highlighter.findAnnotationSpans(arg);

                    if (spans.length > 0) {
                        const selectedDOMElement = spans[0];
                        const selectedAnnotation = spans[0].annotation;

                        this.setState({
                            selectedAnnotation,
                            selectedDOMElement,
                        });
                    }
                }
            }
        );
    };

    setAnnotations = (annotations) => {
        this.highlighter.clear();
        this.relationsLayer.clear();

        const clones = annotations.map((a) => a.clone());

        return this.highlighter
            .init(clones)
            .then(() => this.relationsLayer.init(clones));
    };

    setMode = (mode) => {
        if (mode === 'RELATIONS') {
            this.clearState();

            this.selectionHandler.enabled = false;

            this.relationsLayer.readOnly = false;
            this.relationsLayer.startDrawing();
        } else {
            this.setState({ selectedRelation: null });

            this.selectionHandler.enabled = true;

            this.relationsLayer.readOnly = true;
            this.relationsLayer.stopDrawing();
        }
    };

    get readOnly() {
        return this.state.readOnly;
    }

    set readOnly(readOnly) {
        this.selectionHandler.readOnly = readOnly;
        // Note: relationsHandler.readOnly should be set by setMode.
        this.setState({ readOnly });
    }

    get widgets() {
        return this.state.widgets;
    }

    set widgets(widgets) {
        this.setState({ widgets });
    }

    get disableEditor() {
        return this.state.editorDisabled;
    }

    set disableEditor(disabled) {
        this.setState({ editorDisabled: disabled });
    }

    render() {
        // The editor should open under normal conditions - annotation was selected, no headless mode
        const open =
            (this.state.selectedAnnotation || this.state.selectedRelation) &&
            !this.state.editorDisabled;

        const readOnly =
            this.state.readOnly || this.state.selectedAnnotation?.readOnly;

        return (
            open && (
                <>
                    {this.state.selectedAnnotation && (
                        <Editor
                            ref={this._editor}
                            autoPosition={this.props.config.editorAutoPosition}
                            wrapperEl={this.props.wrapperEl}
                            annotation={this.state.selectedAnnotation}
                            selectedElement={this.state.selectedDOMElement}
                            readOnly={readOnly}
                            allowEmpty={this.props.config.allowEmpty}
                            widgets={this.state.widgets}
                            env={this.props.env}
                            onChanged={this.onChanged}
                            onAnnotationCreated={this.onCreateOrUpdateAnnotation(
                                'onAnnotationCreated'
                            )}
                            onAnnotationUpdated={this.onCreateOrUpdateAnnotation(
                                'onAnnotationUpdated'
                            )}
                            onAnnotationDeleted={this.onDeleteAnnotation}
                            onCancel={this.onCancelAnnotation}
                        />
                    )}

                    {this.state.selectedRelation && (
                        <RelationEditor
                            relation={this.state.selectedRelation}
                            onRelationCreated={this.onCreateOrUpdateRelation}
                            onRelationUpdated={this.onCreateOrUpdateRelation}
                            onRelationDeleted={this.onDeleteRelation}
                            onCancel={this.closeRelationsEditor}
                            vocabulary={this.props.relationVocabulary}
                        />
                    )}
                </>
            )
        );
    }
}
