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

import IDUtil from "../../../../util/IDUtil";
import { stringContains } from "./_stringHelpers";

import Actions from "./Actions";
import Cursor from "./Cursor";
import Layers, { LayersPropTypes } from "./Layers";
import LayerHeaders from "./LayerHeaders";
import Axis from "./Axis";
import ZoomDragBox from "./ZoomDragBox";

// Main component that combines all sub components to a fully functional timeline
class Timeline extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            // timeline view start
            start: this.props.viewStart ? this.props.viewStart : 0,
            // timeline view end
            end: this.props.viewEnd ? this.props.viewEnd : 30,
            // current position
            position: 1,
            // bounding box of right column
            boundingBox: { x: 0, y: 0, width: 1, height: 1 },
            // layers filtered by search Term
            layers: [],
            // search terms
            searchTerms: [],
            // active layer id
            activeLayerId: null,
            // active section id
            activeSectionId: "",
        };

        // optional settings
        this.autoScrollOffset = this.props.autoScrollOffset
            ? this.props.autoScrollOffset
            : 2;
        this.minDuration = this.props.minDuration ? this.props.minDuration : 3;

        // refs
        this.rightColumnRef = React.createRef();
    }

    componentDidMount() {
        // prepare data
        this.updateLayers();

        // bounding box
        this.updateBoundingBox();

        // request a new bounding box on page resize
        window.addEventListener("resize", this.updateBoundingBox);
    }

    componentWillUnmount() {
        window.removeEventListener("resize", this.updateBoundingBox);
    }

    // set new bounding box to the state
    updateBoundingBox = () => {
        this.setState({
            boundingBox: this.rightColumnRef.current.getBoundingClientRect(),
        });
    };

    // limit the given position value to the max start and end time
    limit = (position) =>
        Math.min(this.props.end, Math.max(this.props.start, position));

    // set cursor position
    // call parent in this.props.setPosition if callParent
    setPosition = (position, callParent = true, cursorToCenter = false) => {
        // set position
        position = this.limit(position);
        this.setState({ position });

        const autoScrollOffset = Math.min(
            Math.abs(this.props.start - this.state.position),
            Math.min(
                Math.abs(this.props.end - this.state.position),
                this.autoScrollOffset / this.getPixelsPerSecond()
            )
        );

        // cursor to timeline center
        if (cursorToCenter) {
            const duration = this.state.end - this.state.start;
            const mid = this.state.start + duration / 2;

            if (
                position - duration * 0.1 > this.state.end ||
                position + duration * 0.1 < this.state.start
            ) {
                // put in center immediately
                this.onMove(position - mid);
            } else if (position > mid) {
                // animate to center
                this.onMove(1 / this.getPixelsPerSecond());
            }
        }

        // auto scroll start
        if (position - autoScrollOffset < this.state.start) {
            this.onMove(position - autoScrollOffset - this.state.start);
        }

        // auto scroll end
        if (position + autoScrollOffset > this.state.end) {
            this.onMove(position + autoScrollOffset - this.state.end);
        }

        this.autoSelectActive();

        // external callback
        if (callParent) {
            this.props.setPosition(position);
        }
    };

    // can be called externally using a ref
    setPositionExternal = (pos) => {
        this.setPosition(pos, false, true);
    };

    autoSelectActive() {
        // auto select the active section in the active layer
        if (this.state.activeLayerId) {
            const layer = this.getLayerById(this.state.activeLayerId);

            if (layer) {
                for (let i = layer.sections.length - 1, section; i >= 0; i--) {
                    section = layer.sections[i];
                    if (
                        section.start <= this.state.position &&
                        section.end > this.state.position
                    ) {
                        this.setState({
                            activeSectionId: section.id,
                        });
                        break;
                    }
                }
            }
        }
    }

    selectLayer = (activeLayerId) => {
        this.setState({ activeLayerId }, () => {
            this.autoSelectActive();
        });
    };

    // Get active layer id
    // can be called externally using a ref
    getActiveLayer = () => {
        return this.state.activeLayerId;
    };

    setStart = (start) =>
        this.setState({
            start: this.limit(start),
        });

    setEnd = (end) =>
        this.setState({
            end: this.limit(end),
        });

    // move the timeline by the given step
    onMove = (step, autoScroll = false) => {
        // calculate new start, end
        const duration = this.state.end - this.state.start;
        let start = this.limit(this.state.start + step);
        let end = start + duration;
        if (end > this.props.end) {
            start -= end - this.props.end;
            end = this.props.end;
        }

        // auto scroll

        // get scroll offset
        let autoScrollOffset =
            this.autoScrollOffset / this.getPixelsPerSecond();
        // - make scroll offset 0 near start/end
        if (
            Math.min(
                this.props.end - this.state.position,
                this.state.position - this.props.start
            ) < autoScrollOffset
        ) {
            autoScrollOffset = 0;
        }

        let position =
            autoScroll && start > this.state.position
                ? start
                : autoScroll && end < this.state.position
                ? end
                : this.state.position;
        if (autoScroll && position !== this.state.position) {
            this.setPosition(position, true);
        }

        // set the state
        this.setState({ start, end });
    };

    // zoom the timeline by the given factor
    // perc(entage) indicates where is zoomed, so we zoom in/out on perc% of the view
    onZoom = (factor, perc) => {
        const duration = this.state.end - this.state.start;
        if (duration < this.minDuration && factor > 1) {
            return;
        }
        const newDuration = duration * factor;
        const durationDiff = newDuration - duration;

        let start = this.limit(this.state.start + durationDiff * perc);
        let end = this.limit(this.state.end - durationDiff * (1 - perc));

        // handle case when heavy scrolling on touchpad results in incorrect values
        if (start > end) {
            if (end === 0) {
                end = this.minDuration;
            }
            start = end - this.minDuration;
        }

        this.setState({ start, end });
    };

    // use the given search term to extract searchterms that are stored in the state
    onSearch = (searchTerm) => {
        this.setState({
            searchTerms: searchTerm
                ? searchTerm.trim().toLowerCase().split(" ")
                : [],
        });
    };

    // helper: Get a layer by the given id
    getLayerById = (id) => {
        const { layers } = this.state;

        for (let i = 0, len = layers.length; i < len; i++) {
            if (layers[i].id === id) {
                return layers[i];
            }
        }
        return null;
    };

    // helper: Get a Section by the given layer and section ids
    getSectionById = (layerId, sectionId) => {
        const layer = this.getLayerById(layerId);
        if (!layer) {
            return null;
        }

        for (let i = 0, len = layer.sections.length; i < len; i++) {
            if (layer.sections[i].id === sectionId) {
                return layer.sections[i];
            }
        }
        return null;
    };

    // zoom to active section
    onZoomToSelected = () => {
        const { activeLayerId, activeSectionId } = this.state;

        const section = this.getSectionById(activeLayerId, activeSectionId);
        if (!section) {
            return;
        }

        this.setState({
            start: this.limit(section.start),
            end: this.limit(section.end),
        });
    };

    // select active layer and section
    onSelect = (activeLayerId, activeSectionId) => {
        this.setState({
            activeLayerId,
            activeSectionId,
        });

        const section = this.getSectionById(activeLayerId, activeSectionId);
        if (!section) {
            return;
        }

        this.setPosition(section.start);

        // keep section (start) in screen
        if (section.start < this.state.start) {
            this.onMove(section.start - this.state.start);
        } else if (section.start >= this.state.end) {
            section.end - section.start < this.state.end - this.state.start
                ? this.onMove(
                      section.start +
                          (section.end - section.start) -
                          this.state.end
                  )
                : this.onMove(section.start - this.state.start);
        }
    };

    // select next section in active layer
    // in case there are searchterms, go the next match
    onSelectNext = () => {
        const { activeLayerId, activeSectionId, searchTerms } = this.state;

        const layer = this.getLayerById(activeLayerId);
        if (!layer) {
            return;
        }

        const useSearch = searchTerms.length > 0;

        // select next
        for (
            let i = 0, hit = false, len = layer.sections.length;
            i < len;
            i++
        ) {
            // get index of active section
            if (layer.sections[i].id === activeSectionId) {
                hit = true;
                continue;
            }

            // select section on first hit
            if (
                hit &&
                (!useSearch ||
                    stringContains(layer.sections[i].rawData, searchTerms))
            ) {
                this.onSelect(activeLayerId, layer.sections[i].id);
                break;
            }
        }
    };

    // select previous section in active layer
    // in case there are searchterms, go the previous match
    onSelectPrev = () => {
        const { activeLayerId, activeSectionId, searchTerms } = this.state;

        const layer = this.getLayerById(activeLayerId);
        if (!layer) {
            return;
        }

        const useSearch = searchTerms.length > 0;

        // select prev
        for (let i = layer.sections.length - 1, hit = false; i >= 0; i--) {
            // get index of active section
            if (layer.sections[i].id === activeSectionId) {
                hit = true;
                continue;
            }

            // select section on first hit
            if (
                hit &&
                (!useSearch ||
                    stringContains(layer.sections[i].rawData, searchTerms))
            ) {
                this.onSelect(activeLayerId, layer.sections[i].id);
                break;
            }
        }
    };

    // helper: pixels per second, needed for calculations
    // can be called externally using a ref
    getPixelsPerSecond = () =>
        this.state.boundingBox.width / (this.state.end - this.state.start);

    // update the layers data in the state
    // indicated of sections match the current search terms
    updateLayers() {
        const { searchTerms } = this.state;

        const useSearch = searchTerms.length > 0;

        const layers = this.props.layers.map((layer) =>
            Object.assign({}, layer, {
                sections: layer.sections.map((section) => {
                    section.match = useSearch
                        ? stringContains(section.rawData, searchTerms)
                        : true;
                    return section;
                }),
            })
        );
        this.setState({ layers });
    }

    componentDidUpdate(prevProps, prevState) {
        // Check if layers should be updated
        if (
            // new search term
            prevState.searchTerms !== this.state.searchTerms ||
            // new layer data
            prevProps.layers !== this.props.layers
        ) {
            this.updateLayers();
        }
    }

    render() {
        const pixelsPerSecond = this.getPixelsPerSecond();

        // Vars from state
        const {
            start,
            end,
            position,
            boundingBox,
            layers,
            activeLayerId,
            activeSectionId,
        } = this.state;

        // Vars from props
        const { highlightSectionId, onDoubleClick } = this.props;

        return (
            <div className={IDUtil.cssClassName("timeline")}>
                {/* Actions */}
                <Actions
                    onSearch={this.onSearch}
                    onMove={this.onMove}
                    onZoom={this.onZoom}
                    duration={end - start}
                    onZoomToSelected={
                        activeLayerId != null && activeSectionId
                            ? this.onZoomToSelected
                            : null
                    }
                    onSelectNext={
                        activeLayerId != null && activeSectionId
                            ? this.onSelectNext
                            : null
                    }
                    onSelectPrev={
                        activeLayerId != null && activeSectionId
                            ? this.onSelectPrev
                            : null
                    }
                />

                {/* Row that contains the left/right columns */}
                <div className="tl-row">
                    {/* Left column */}
                    <div className="tl-left">
                        <div className="tl-annotation-actions">
                            {this.props.actions}
                        </div>
                        <LayerHeaders
                            layers={layers}
                            activeLayerId={activeLayerId}
                            onClick={this.selectLayer}
                        />
                    </div>

                    {/* Right column */}
                    <div className="tl-right" ref={this.rightColumnRef}>
                        {/* Zoom Drag Box */}
                        <ZoomDragBox
                            start={start}
                            position={position}
                            pixelsPerSecond={pixelsPerSecond}
                            boundingBox={boundingBox}
                            height={40}
                            setPosition={this.setPosition}
                            onMove={this.onMove}
                            onZoom={this.onZoom}
                        >
                            {/* Axis */}
                            <Axis
                                start={start}
                                end={end}
                                position={position}
                                pixelsPerSecond={pixelsPerSecond}
                                width={boundingBox.width}
                                height={40}
                            />

                            {/* Layers */}

                            <Layers
                                layers={layers}
                                start={start}
                                end={end}
                                pixelsPerSecond={pixelsPerSecond}
                                onSectionClick={this.onSelect}
                                activeLayerId={activeLayerId}
                                highlightSectionId={highlightSectionId}
                            />

                            {/* Cursor */}
                            <Cursor
                                start={start}
                                position={position}
                                pixelsPerSecond={pixelsPerSecond}
                                width={boundingBox.width}
                            />
                        </ZoomDragBox>
                    </div>
                </div>
            </div>
        );
    }
}

Timeline.propTypes = {
    start: PropTypes.number.isRequired,
    end: PropTypes.number.isRequired,
    setPosition: PropTypes.func.isRequired,

    viewStart: PropTypes.number,
    viewEnd: PropTypes.number,
    layers: LayersPropTypes,
    autoScrollOffset: PropTypes.number,
    minDuration: PropTypes.number,
    actions: PropTypes.object,
};

export default Timeline;
