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: '',
            // 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) {
                layer.sections.some(section => {
                    if (
                        section.start <= this.state.position &&
                        section.end > this.state.position
                    ) {
                        this.setState({
                            activeSectionId: section.id
                        });
                        return true;
                    }
                    return false;
                });
            }
        }
    }

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

    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) => {
        const section = this.getSectionById(activeLayerId, activeSectionId);
        if (!section) {
            return;
        }
        this.setState({
            activeLayerId,
            activeSectionId
        });

        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
    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(_, prevState) {
        // in case of new searchTerms, update the layerdata
        if (prevState.searchTerms !== this.state.searchTerms) {
            this.updateLayers();
        }
    }

    render() {
        const pixelsPerSecond = this.getPixelsPerSecond();
        const {
            start,
            end,
            position,
            boundingBox,
            layers,
            activeLayerId,
            activeSectionId
        } = this.state;

        return (
            <div className={IDUtil.cssClassName('timeline')}>
                {/* Actions */}
                <Actions
                    onSearch={this.onSearch}
                    onMove={this.onMove}
                    onZoom={this.onZoom}
                    duration={end - start}
                    onZoomToSelected={
                        this.state.activeLayerId && this.state.activeSectionId
                            ? this.onZoomToSelected
                            : null
                    }
                    onSelectNext={
                        this.state.activeLayerId && this.state.activeSectionId
                            ? this.onSelectNext
                            : null
                    }
                    onSelectPrev={
                        this.state.activeLayerId && this.state.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"></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 */}
                            <div
                                className={IDUtil.cssClassName('tl-layers')}
                                style={{
                                    transform:
                                        'translateX(' +
                                        -start * pixelsPerSecond +
                                        'px)'
                                }}
                            >
                                <Layers
                                    layers={layers}
                                    start={start}
                                    end={end}
                                    pixelsPerSecond={pixelsPerSecond}
                                    onSelect={this.onSelect}
                                    activeLayerId={activeLayerId}
                                    activeSectionId={activeSectionId}
                                />
                            </div>

                            {/* 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,
    viewStart: PropTypes.number,
    viewEnd: PropTypes.number,
    setPosition: PropTypes.func.isRequired,

    layers: LayersPropTypes,

    autoScrollOffset: PropTypes.number,
    minDuration: PropTypes.number
};

export default Timeline;
