import React from "react";
import PropTypes from "prop-types";

import FlexPlayerUtil from "../../../util/FlexPlayerUtil";
import MediaObject from "../../../model/MediaObject";
import EditableText from "../../shared/EditableText";

import { ANNOTATION_TARGET } from "../../../util/AnnotationConstants";
import { AnnotationEvents } from "../AnnotationClient";
import MediaEvents from "../_MediaEvents";
import { ResourceViewerContext } from "../ResourceViewerContext";
import Strings from "../_Strings";
import { getSegmentTitle, getSelectionTitle } from "../AnnotationHelpers";

import Timeline from "./timeline/Timeline";
import {
    UserSegmentSection,
    ContentSegmentSection,
    ASRSentenceSection,
    ASRWordCloudSection,
} from "./timeline/Sections";
import SectionEdit from "./timeline/SectionEdit";
import KeyboardInteraction from "./KeyboardInteraction";

const LAYER_CONTENT_SEGMENTS_ID = -102;

/**
 *   This component builds the timeline for the given MediaObject
 *   and media content duration
 */

export default class TimelineView extends React.PureComponent {
    static contextType = ResourceViewerContext;

    constructor(props) {
        super(props);

        this.timelineRef = React.createRef();

        this.currentPosition = 0;

        this.state = {
            userLayers: [],
            contentLayers: [],
        };

        this.annotationEvents = [
            AnnotationEvents.ON_EDIT,
            AnnotationEvents.ON_SET_ANNOTATION,
            AnnotationEvents.ON_SET_SELECTION,
            AnnotationEvents.ON_SAVE,
            AnnotationEvents.ON_DELETE,
            AnnotationEvents.ON_CHANGE_TARGET,
        ];
    }

    /**
     * React lifecycle functions
     */

    componentDidMount = () => {
        // Bind to player position updates
        this.context.mediaEvents.bind(
            MediaEvents.PLAYER_POS,
            this.updateTimelinePosition
        );

        // Bind to annotation updates
        this.annotationEvents.forEach((event) => {
            this.context.annotationClient.events.bind(
                event,
                this.updateAnnotations
            );
        });

        // Build layers
        this.buildLayers();

        // Keyboardinteractions for valid users with active project
        if (this.hasValidUserAndProject()) {
            this.initKeyboardInteractions();
        }
    };

    componentWillUnmount = () => {
        // Unbind player updates
        this.context.mediaEvents.unbind(
            MediaEvents.PLAYER_POS,
            this.updateTimelinePosition
        );

        // Unbind annotation updates
        this.annotationEvents.forEach((event) => {
            this.context.annotationClient.events.unbind(
                event,
                this.updateAnnotations
            );
        });

        // keyboard interaction
        if (this.keyboardInteraction) {
            this.keyboardInteraction.destroy();
        }
    };

    componentDidUpdate(prevProps) {
        // Update the layers in case the mediaObject is changed
        // or when the active AnnotationTypes did change
        if (prevProps.mediaObject.id !== this.props.mediaObject.id) {
            this.buildContentLayers();
        }

        // Update the layers in case active AnnotationTypes changed
        if (
            prevProps.activeAnnotationTypes !== this.props.activeAnnotationTypes
        ) {
            this.buildUserLayers();
        }
    }

    initKeyboardInteractions() {
        this.keyboardInteraction = new KeyboardInteraction({
            context: this.context,
        });
    }

    // Update segment with given selection
    // If the selection is null, delete the segment
    updateSegment = async (segment, selection) => {
        // When the selection is null: delete the segment
        if (selection == null) {
            this.context.annotationClient.delete(segment);
            return;
        }

        // make the segment the active annotation, so it will be updated
        this.context.annotationClient.activeAnnotation = segment;

        // set the active selection
        this.context.annotationClient.activeSelection = selection;

        // Update the segment
        await this.context.annotationClient.saveSelection(selection);
    };

    withinRange = (x) => {
        return Math.max(0, Math.min(this.props.duration, x));
    };

    snapToSegments = (
        x,
        segments,
        currentSegment,
        layerId = null,
        offset = 1.25
    ) => {
        if (!currentSegment) {
            return;
        }

        // If segments == null, snap to all user segments
        if (segments == null) {
            segments = this.context.annotationClient.annotations
                ? this.context.annotationClient.annotations.filter(
                      (annotation) =>
                          annotation.target.type ===
                              ANNOTATION_TARGET.SEGMENT &&
                          (layerId === null ||
                              annotation.target.layerId === layerId)
                  )
                : [];
        }

        offset /= this.getPixelsPerSecond();

        segments.some((segment) => {
            if (segment.id == currentSegment.id) {
                return false;
            }
            // start
            if (
                Math.abs(segment.target.selector.refinedBy.start - x) < offset
            ) {
                x = segment.target.selector.refinedBy.start;
                return true;
            }
            // end
            if (Math.abs(segment.target.selector.refinedBy.end - x) < offset) {
                x = segment.target.selector.refinedBy.end;
                return true;
            }
            return false;
        });

        return x;
    };

    /**
     * Update annotations (called from events, section edits etc)
     */
    updateAnnotations = () => {
        this.buildUserLayers();
    };

    updateUserAnnotationLayer = (layerId) => {
        const layer = this.state.userLayers.find(
            (layer) => layer.id === layerId
        );

        // failsafe
        if (!layer) {
            this.updateAnnotations();
            console.error("Could not find layer with id", layerId);
            return;
        }

        const layerTitle = this.context.annotationClient.segmentLayers.getLayerTitle(
            layerId
        );
        const userLayers = [...this.state.userLayers];
        userLayers[userLayers.indexOf(layer)] = this.buildUserLayer(
            layerId,
            layerTitle
        );

        this.setState({ userLayers });
    };

    /**
     * Operate directly on timeline ref
     */
    updateTimelinePosition = (pos) => {
        this.currentPosition = pos;
        if (this.timelineRef.current) {
            this.timelineRef.current.setPositionExternal(pos);
        }
    };

    getPixelsPerSecond = () => {
        return this.timelineRef.current
            ? this.timelineRef.current.getPixelsPerSecond()
            : null;
    };

    updatePlayerPos = (pos) => {
        this.context.mediaEvents.trigger(MediaEvents.SET_PLAYER_POS, pos);
    };

    showContentAnnotations = () => {
        this.context.mediaEvents.trigger(MediaEvents.SHOW_CONTENT_ANNOTATIONS);
    };

    /**
     *  Timeline layers
     */

    selectTimelineLayer = (layerId) => {
        if (this.timelineRef.current) {
            this.timelineRef.current.selectLayer(layerId);
        }
    };

    getActiveLayerId = () => {
        if (this.timelineRef.current) {
            return this.timelineRef.current.getActiveLayer();
        }
        return null;
    };

    /* ----------------------------- USER LAYERS ------------------------ */

    getAnnotateSegmentButton = (
        annotationClient,
        showAnnotationPopup,
        layerId
    ) => {
        // Only render the button when there is an active annotation of type segment
        return annotationClient.activeAnnotation &&
            annotationClient.activeAnnotation.target &&
            annotationClient.activeAnnotation.target.layerId == layerId &&
            annotationClient.activeAnnotation.target.type ===
                ANNOTATION_TARGET.SEGMENT ? (
            <div
                className="annotate-segment-button"
                title={Strings.ANNOTATE_SEGMENT_BUTTON_TITLE}
                onClick={() => {
                    showAnnotationPopup();
                }}
            />
        ) : null;
    };

    getDeleteSegmentButton = (annotationClient, layerId) => {
        // Only render the button when there is an active annotation of type segment
        return annotationClient.activeAnnotation &&
            annotationClient.activeAnnotation.target &&
            annotationClient.activeAnnotation.target.layerId == layerId &&
            annotationClient.activeAnnotation.target.type ===
                ANNOTATION_TARGET.SEGMENT ? (
            <div
                className="delete-segment-button"
                title={Strings.DELETE_SEGMENT_BUTTON_TITLE}
                onClick={() => {
                    if (!confirm(Strings.SEGMENT_DELETE_CONFIRM)) {
                        return;
                    }
                    // sending null to the update function results in the segment getting deleted
                    annotationClient.delete(annotationClient.activeAnnotation);
                }}
            />
        ) : null;
    };

    getAddSegmentButton = (annotationClient, layerId) => (
        <div
            className="add-segment-button"
            title={Strings.TIMELINE_LAYER_USER_SEGMENTS_ADD_BUTTON_TITLE}
            onMouseDown={async (e) => {
                // set activeAnnotation to null, so a new root annotation is created & activated
                annotationClient.activeAnnotation = null;

                let selection;
                // shift key = concat new segment
                if (e.shiftKey) {
                    const prevSectionEnd = annotationClient.getSegmentEndBefore(
                        this.currentPosition,
                        layerId
                    );
                    selection = annotationClient.newTemporalSegment(
                        prevSectionEnd,
                        this.currentPosition
                    );
                } else {
                    selection = annotationClient.newTemporalSegment(
                        this.currentPosition,
                        this.currentPosition + 1
                    );
                }

                // Create and activate a new segment, provide layerId
                await annotationClient.saveSelection(
                    selection,
                    true,
                    true,
                    layerId
                );
            }}
            onMouseUp={async (e) => {
                // on mouse up with Ctrl-key: set active selection end-position to currentPosition
                if (
                    e.ctrlKey &&
                    annotationClient.activeAnnotation &&
                    annotationClient.activeSelection &&
                    annotationClient.activeSelection.end
                ) {
                    annotationClient.activeSelection.end = this.currentPosition;
                    await annotationClient.saveSelection(
                        annotationClient.activeSelection
                    );
                }
            }}
        >
            +
        </div>
    );

    getUserAnnotationLayerTitle(layerTitle, layerId) {
        return (
            <EditableText
                value={layerTitle}
                onChange={async (value) => {
                    await this.context.annotationClient.segmentLayers.renameLayer(
                        layerId,
                        value
                    );
                }}
            />
        );
    }

    getUserSegmentLayer(
        segments,
        mediaObject,
        annotationClient,
        layerId,
        layerTitle,
        duration,
        activeAnnotationTypes,
        showAnnotationPopup
    ) {
        // Segment buttons
        const addSegmentButton = this.getAddSegmentButton(
            annotationClient,
            layerId
        );
        const deleteSegmentButton = this.getDeleteSegmentButton(
            annotationClient,
            layerId
        );
        const annotateSegmentButton = this.getAnnotateSegmentButton(
            annotationClient,
            showAnnotationPopup,
            layerId
        );

        // Buttons in layer header
        const headerChildren = (
            <React.Fragment>
                {addSegmentButton}
                {deleteSegmentButton}
                {annotateSegmentButton}
            </React.Fragment>
        );

        // On layer double click:
        // - Add a new segment on Shift+Click
        // - Listen to mouse move to set end position
        const onLayerDoubleClick = async (position, e) => {
            // Check if the double click is on the empty layer
            const clickOnEmptyLayer =
                e.target.className.indexOf("bg__tl-layer") > -1;

            // When not on empty layer; a user segment is clicked
            // so just show the Annotation Popup and return
            if (!clickOnEmptyLayer) {
                showAnnotationPopup();
                return;
            }

            // When clicked in empty layer space; Add a new annotation by dragging:

            // set activeAnnotation to null, so a new root annotation is created & activated
            annotationClient.activeAnnotation = null;

            annotationClient.newTemporalSegment(position, position + 1);

            // Check if mouse goes up during async
            let cancel = false;
            const mouseCancel = () => {
                cancel = true;
                document.removeEventListener("mouseup", mouseCancel);
            };
            document.addEventListener("mouseup", mouseCancel);

            // Store new segment, include layerId
            await annotationClient.saveSelection(
                annotationClient.activeSelection,
                true,
                true,
                layerId
            );

            // Mouse action has been canceled; return
            if (cancel) {
                return;
            }

            // Continue editing the new segment
            document.removeEventListener("mouseup", mouseCancel);

            let lastX = e.pageX;

            // Get current segment object in annotation array so we can edit it directly
            let segment = null;
            annotationClient.annotations.some((annotation) => {
                if (
                    annotation.target &&
                    annotation.target.type === ANNOTATION_TARGET.SEGMENT &&
                    annotation.id === annotationClient.activeAnnotation.id
                ) {
                    segment = annotation;
                    return true;
                }
                return false;
            });

            if (!segment) {
                return;
            }

            // Set end to start+0.01, so we really start dragging from the original doubleClick position
            segment.target.selector.refinedBy.end =
                segment.target.selector.refinedBy.start + 0.01;

            // Listening to mouse changes and set segment end accordingly
            const onMouseMove = (e) => {
                if (!segment.target) {
                    return;
                }
                const selection = segment.target.selector.refinedBy;

                selection.end = this.snapToSegments(
                    selection.end +
                        (e.pageX - lastX) / this.getPixelsPerSecond(),
                    segments,
                    segment,
                    layerId
                );

                // Swap start/end in case of negative length
                if (selection.end < selection.start) {
                    const _start = selection.start;
                    selection.start = selection.end;
                    selection.end = _start;
                }

                selection.start = this.withinRange(selection.start);
                selection.end = this.withinRange(selection.end);

                lastX = e.pageX;
                this.updateAnnotations();
            };

            const onMouseUp = (e) => {
                document.removeEventListener("mousemove", onMouseMove);
                document.removeEventListener("mouseup", onMouseUp);
                this.updateSegment(segment, segment.target.selector.refinedBy);
            };

            document.addEventListener("mousemove", onMouseMove);
            document.addEventListener("mouseup", onMouseUp);
        };

        // Return the layer
        return {
            id: layerId,
            title: this.getUserAnnotationLayerTitle(layerTitle, layerId),
            className: "user-segment",
            headerChildren: headerChildren,
            onDoubleClick: onLayerDoubleClick,
            sections: segments
                .filter((segment) => segment.target.selector.refinedBy)
                .sort(
                    (a, b) =>
                        a.target.selector.refinedBy.start -
                        b.target.selector.refinedBy.start
                )
                .map((segment) => {
                    // Create title segments (based on active annotations)
                    const label = getSegmentTitle(
                        segment,
                        activeAnnotationTypes,
                        false
                    );

                    const title = getSelectionTitle(segment);

                    // Return segment data
                    return {
                        id:
                            "tl_user_seg_" +
                            segment.id +
                            "_" +
                            segment.modified,
                        start: segment.target.selector.refinedBy.start,
                        end: segment.target.selector.refinedBy.end,
                        rawData: label.toLowerCase(),
                        highlight:
                            annotationClient.activeAnnotation &&
                            annotationClient.activeAnnotation.id == segment.id,
                        data: (
                            <SectionEdit
                                getPixelsPerSecond={this.getPixelsPerSecond}
                                updateAnnotationLayer={
                                    this.updateUserAnnotationLayer
                                }
                                updatePlayerPos={this.updatePlayerPos}
                                updateSegment={this.updateSegment}
                                withinRange={this.withinRange}
                                snapToSegments={this.snapToSegments}
                                selectTimelineLayer={this.selectTimelineLayer}
                                duration={duration}
                                segment={segment}
                                mediaObject={mediaObject}
                                annotationClient={annotationClient}
                            >
                                <UserSegmentSection
                                    label={label ? label : title}
                                    title={title}
                                    onClick={
                                        annotationClient.setActiveAnnotation
                                    }
                                    segment={segment}
                                />
                            </SectionEdit>
                        ),
                    };
                }),
        };
    }

    // Build all user layers and update state
    buildUserLayers = () => {
        // A valid user is required to build the UserLayers
        if (!this.hasValidUserAndProject()) {
            return;
        }

        const layers = this.context.annotationClient.segmentLayers.getLayersSorted();

        const userLayers = layers.map((layer) =>
            this.buildUserLayer(layer.id, layer.title)
        );

        this.setState({ userLayers });
    };

    // Build a single user layer with given layerId and title
    buildUserLayer(layerId, title) {
        const { annotationClient, activeAnnotationTypes } = this.context;

        const { mediaObject, duration, showAnnotationPopup } = this.props;

        return this.getUserSegmentLayer(
            annotationClient.segmentLayers.getSegments(layerId),
            mediaObject,
            annotationClient,
            layerId,
            title,
            duration,
            activeAnnotationTypes,
            showAnnotationPopup
        );
    }

    /* ----------------------------- CONTENT LAYERS ------------------------ */

    getContentSegmentLayer(mediaObject) {
        return {
            id: LAYER_CONTENT_SEGMENTS_ID,
            title: Strings.TIMELINE_LAYER_CONTENT_SEGMENTS_TITLE,
            description: Strings.TIMELINE_LAYER_CONTENT_SEGMENTS_HELP,
            className: "content-segment",
            sections: mediaObject.segments
                ? mediaObject.segments
                      .filter((segment) => !segment.programSegment)
                      .sort((a, b) => a.start - b.start)
                      .map((segment, index) => ({
                          id: "tl_content_seg_" + index,
                          start: FlexPlayerUtil.timeRelativeToOnAir(
                              segment.start,
                              mediaObject
                          ),
                          end: FlexPlayerUtil.timeRelativeToOnAir(
                              segment.end,
                              mediaObject
                          ),
                          rawData: segment.title
                              ? segment.title.toLowerCase()
                              : "",
                          data: <ContentSegmentSection title={segment.title} />,
                      }))
                : [],
        };
    }

    getTranscriptWordCloudLayer = (layerIndex, transcriptType, transcript) => {
        if(!transcript || transcript.lines.length === 0) return null;

        // ASR wordcloud every %step% seconds
        const step = 30; //sec -- if you change this line; you may want to update the description in _Strings.js
        const buckets = {};

        if (transcript.lines[0].wordTimes) { //for ASR transcripts with timings per word as well
            let wordTimes = [];
            let words = [];

            // collect all words/wordtimes
            transcript.lines.forEach(item => {
                words = words.concat(item.text.split(" "));
                wordTimes = wordTimes.concat(item.wordTimes);
            });

            // create buckets from wordtimes
            wordTimes.forEach((t, index) => {
                // relative time
                //t = FlexPlayerUtil.timeRelativeToOnAir(t / 1000, mediaObject);
                t = t / 1000;
                const bucketIndex = Math.floor(t / step);

                // create bucket
                if (!(bucketIndex in buckets)) {
                    buckets[bucketIndex] = {
                        id: "tl_asr_wo_" + bucketIndex,
                        start: bucketIndex * step,
                        end: bucketIndex * step + step,
                        words: [],
                    };
                }

                // add word
                buckets[bucketIndex].words.push(words[index]);
            });
        } else { //these transcrips only have a start and end time per block of text
            transcript.lines.forEach(item => {
                const bucketIndex = Math.floor((item.start / 1000) / step);
                if (!(bucketIndex in buckets)) {
                    buckets[bucketIndex] = {
                        id: "tl_asr_wo_" + bucketIndex,
                        start: bucketIndex * step,
                        end: bucketIndex * step + step,
                        words: [],
                    };
                }
                buckets[bucketIndex].words.push(...item.text.split(" "));
            });
        }

        // create sections from buckets
        const bucketSections = Object.values(buckets).map(bucket => {
            bucket.rawData = bucket.words.join(" ").toLowerCase();
            bucket.data = (
                <ASRWordCloudSection
                    size={20}
                    words={bucket.words}
                    onClick={this.showContentAnnotations}
                />
            );
            return bucket;
        });

        // create the ASR words layer
        return {
            id: layerIndex,
            title: transcript.title + " " + Strings.TIMELINE_LAYER_ASR_WORDS_TITLE,
            description: Strings.TIMELINE_LAYER_ASR_WORDS_HELP,
            className: "asr-words",
            height: 150,
            sections: bucketSections,
        };
    };

    getTranscriptLayer = (layerIndex, transcriptType, transcript) => {
        if(!transcript || transcript.lines.length === 0) return null;

        const sections = [];
        transcript.lines.forEach((line, index) => {
            let sectionId = "tl_"+transcriptType+"_se_" + index;
            const i = sections.findIndex(x => x.id == sectionId);
            if(i <= -1){
                sections.push({
                    id: sectionId,
                    start: line.start / 1000, //convert to seconds
                    end: line.end / 1000,
                    rawData: line.text,
                    data: (
                        <ASRSentenceSection
                            title={line.text}
                            opacity="90%"
                        />
                    )
                });
            }
        });
        return {
            id: layerIndex,
            title: transcript.title,
            description: transcript.title,
            className: "asr-sentence",
            sections: sections
        }
    };

    buildContentLayers = () => {
        const context = this.context;
        const { mediaObject } = this.props;

        // Default content layers
        let contentLayers = [
            this.getContentSegmentLayer(mediaObject)
        ];

        // Only apply when using DAAN collection
        const activeTranscripts = this.context.getActiveTranscripts();

        if(activeTranscripts) {
            let layerIndex = -103;
            activeTranscripts.forEach(transcript => {
                const transcriptLayer = this.getTranscriptLayer(
                    layerIndex--,
                    transcript.type,
                    transcript
                );
                const transcriptWordCloudLayer = this.getTranscriptWordCloudLayer(
                    layerIndex--,
                    transcript.type,
                    transcript
                );
                if(transcriptLayer)contentLayers.push(transcriptLayer);
                if(transcriptLayer)contentLayers.push(transcriptWordCloudLayer);
            });
        }

        this.setState({
            contentLayers: contentLayers
        });
    };

    buildLayers = () => {
        this.buildUserLayers();
        this.buildContentLayers();
    };

    renderNewLayerButton = () => {
        return (
            <div
                key="new-layer-button"
                className="button-new-layer btn btn-primary"
                onClick={
                    this.context.annotationClient.segmentLayers.addEmptyLayer
                }
            >
                {Strings.BUTTON_ADD_USER_LAYER}
            </div>
        );
    };

    hasValidUserAndProject = () => {
        return (
            this.context.user &&
            this.context.user.id !== "ANONYMOUS" &&
            this.context.activeProject
        );
    };

    deleteActiveLayer = () => {
        const layerId = this.getActiveLayerId();

        this.context.annotationClient.segmentLayers.deleteLayerWithCheck(
            layerId
        );
    };

    renderDeleteLayerButton = () => {
        return (
            <div
                onClick={this.deleteActiveLayer}
                className="delete-layer-button"
                key="delete-layer-button"
                title={Strings.BUTTON_DELETE_ACTIVE_USER_LAYER_HELP}
            />
        );
    };

    getTimelineActions = () => {
        if (this.hasValidUserAndProject()) {
            return (
                <React.Fragment>
                    {this.renderNewLayerButton()}
                    {this.renderDeleteLayerButton()}
                </React.Fragment>
            );
        }
        return null;
    };

    render = () => {
        const { mediaObject, duration } = this.props;

        // Combine layers
        const layers = [
            ...this.state.userLayers,
            ...this.state.contentLayers.filter(
                (layer) => layer && layer.sections.length
            ),
        ];

        // If there are no layers, and no logged in user, don't display the timeline
        if (layers.length == 0 && !this.hasValidUserAndProject()) {
            return null;
        }

        const actions = this.getTimelineActions();

        // render timeline
        return (
            <Timeline
                key={mediaObject.assetId}
                ref={this.timelineRef}
                start={FlexPlayerUtil.timeRelativeToOnAir(0, mediaObject)}
                end={duration}
                viewStart={0}
                viewEnd={
                    // Always show full first segment, or at least 60 sec; at max 'duration' seconds
                    Math.min(
                        duration,
                        Math.max(
                            60,
                            layers.length > 0 && layers[0].sections.length > 0
                                ? layers[0].sections[0].end
                                : 60
                        )
                    )
                }
                layers={layers}
                setPosition={this.updatePlayerPos}
                actions={actions}
            />
        );
    };
}

TimelineView.propTypes = {
    mediaObject: MediaObject.getPropTypes(true), // Current mediaobject
    duration: PropTypes.number.isRequired, // duration of media content,
    activeAnnotationTypes: PropTypes.arrayOf(PropTypes.string),
};
