import { action, computed, intercept, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc';
import { Copy } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { listSpec } from '../../../fields/Schema';
import { Cast, NumCast } from '../../../fields/Types';
import { Transform } from '../../util/Transform';
import { ObservableReactComponent } from '../ObservableReactComponent';
import { Region, RegionData, RegionHelpers } from './Region';
import { Timeline } from './Timeline';
import './Track.scss';

interface IProps {
    timeline: Timeline;
    animatedDoc: Doc;
    currentBarX: number;
    transform: Transform;
    collection: Doc;
    time: number;
    tickIncrement: number;
    tickSpacing: number;
    timelineVisible: boolean;
    changeCurrentBarX: (x: number) => void;
}

@observer
export class Track extends ObservableReactComponent<IProps> {
    @observable private _inner = React.createRef<HTMLDivElement>();
    @observable private _currentBarXReaction: any = undefined;
    @observable private _timelineVisibleReaction: any = undefined;
    @observable private _autoKfReaction: any = undefined;
    @observable private _newKeyframe: boolean = false;
    private readonly MAX_TITLE_HEIGHT = 75;
    @observable private _trackHeight = 0;
    private primitiveWhitelist = ['x', 'y', '_freeform_panX', '_freeform_panY', '_width', '_height', '_rotation', 'opacity', '_layout_scrollTop'];
    private objectWhitelist = ['data'];

    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    @computed private get regions() {
        return DocListCast(this._props.animatedDoc.regions);
    }
    @computed private get time() {
        return NumCast(RegionHelpers.convertPixelTime(this._props.currentBarX, 'mili', 'time', this._props.tickSpacing, this._props.tickIncrement));
    }

    componentDidMount() {
        DocListCastAsync(this._props.animatedDoc.regions).then(regions => {
            if (!regions) this._props.animatedDoc.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff
            //these two lines are exactly same from timeline.tsx
            const relativeHeight = window.innerHeight / 20;
            runInAction(() => (this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT)); //for responsiveness
            this._timelineVisibleReaction = this.timelineVisibleReaction();
            this._currentBarXReaction = this.currentBarXReaction();
            if (DocListCast(this._props.animatedDoc.regions).length === 0) this.createRegion(this.time);
            this._props.animatedDoc.hidden = false;
            this._props.animatedDoc.opacity = 1;
            // this.autoCreateKeyframe();
        });
    }

    /**
     * mainly for disposing reactions
     */
    componentWillUnmount() {
        this._currentBarXReaction?.();
        this._timelineVisibleReaction?.();
        this._autoKfReaction?.();
    }
    ////////////////////////////////

    getLastRegionTime = () => {
        let lastTime: number = 0;
        let lastRegion: Opt<Doc>;
        this.regions.forEach(region => {
            const time = NumCast(region.position);
            if (lastTime <= time) {
                lastTime = time;
                lastRegion = region;
            }
        });
        return lastRegion ? lastTime + NumCast(lastRegion.duration) : 0;
    };

    /**
     * keyframe save logic. Needs to be changed so it's more efficient
     *
     */
    @action
    saveKeyframe = () => {
        if (this._props.timeline.IsPlaying || !this.saveStateRegion || !this.saveStateKf) {
            this.saveStateKf = undefined;
            this.saveStateRegion = undefined;
            return;
        }
        const keyframes = Cast(this.saveStateRegion.keyframes, listSpec(Doc)) as List<Doc>;
        const kfIndex = keyframes.indexOf(this.saveStateKf);
        const kf = keyframes[kfIndex] as Doc; //index in the keyframe
        if (this._newKeyframe) {
            DocListCast(this.saveStateRegion.keyframes).forEach((kf, index) => {
                this.copyDocDataToKeyFrame(kf);
                kf.opacity = index === 0 || index === 3 ? 0.1 : 1;
            });
            this._newKeyframe = false;
        }
        if (!kf) return;
        // only save for non-fades
        if (this.copyDocDataToKeyFrame(kf)) {
            const leftkf = RegionHelpers.calcMinLeft(this.saveStateRegion, this.time, kf); // lef keyframe, if it exists
            const rightkf = RegionHelpers.calcMinRight(this.saveStateRegion, this.time, kf); //right keyframe, if it exists
            if (leftkf?.type === RegionHelpers.KeyframeType.end) {
                //replicating this keyframe to fades
                const edge = RegionHelpers.calcMinLeft(this.saveStateRegion, this.time, leftkf);
                edge && this.copyDocDataToKeyFrame(edge);
                leftkf && this.copyDocDataToKeyFrame(leftkf);
            }
            if (rightkf?.type === RegionHelpers.KeyframeType.end) {
                const edge = RegionHelpers.calcMinRight(this.saveStateRegion, this.time, rightkf);
                edge && this.copyDocDataToKeyFrame(edge);
                rightkf && this.copyDocDataToKeyFrame(rightkf);
            }
        }
        keyframes[kfIndex] = kf;
        this.saveStateKf = undefined;
        this.saveStateRegion = undefined;
    };

    /**
     * autocreates keyframe
     */
    @action
    autoCreateKeyframe = () => {
        const objects = this.objectWhitelist.map(key => this._props.animatedDoc[key]);
        intercept(this._props.animatedDoc, change => {
            return change;
        });
        return reaction(
            () => {
                return [...this.primitiveWhitelist.map(key => this._props.animatedDoc[key]), ...objects];
            },
            (changed, reaction) => {
                //check for region
                const region = this.findRegion(this.time);
                if (region !== undefined) {
                    //if region at scrub time exist
                    const r = region as RegionData; //for some region is returning undefined... which is not the case
                    if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) {
                        //basically when there is no additional keyframe at that timespot
                        this.makeKeyData(r, this.time, RegionHelpers.KeyframeType.default);
                    }
                }
            },
            { fireImmediately: false }
        );
    };

    // @observable private _storedState:(Doc | undefined) = undefined;
    // /**
    //  * reverting back to previous state before editing on AT
    //  */
    // @action
    // revertState = () => {
    //     if (this._storedState) this.applyKeys(this._storedState);
    // }

    /**
     * Reaction when scrubber bar changes
     * made into function so it's easier to dispose later
     */
    @action
    currentBarXReaction = () => {
        return reaction(
            () => this._props.currentBarX,
            () => {
                const regiondata = this.findRegion(this.time);
                if (regiondata) {
                    this._props.animatedDoc.hidden = false;
                    // if (!this._autoKfReaction) {
                    //     // this._autoKfReaction = this.autoCreateKeyframe();
                    // }
                    this.timeChange();
                } else {
                    this._props.animatedDoc.hidden = true;
                    this._props.animatedDoc !== this._props.collection && (this._props.animatedDoc.opacity = 0);
                    //if (this._autoKfReaction) this._autoKfReaction();
                }
            }
        );
    };

    /**
     * when timeline is visible, reaction is ran so states are reverted
     */
    @action
    timelineVisibleReaction = () => {
        return reaction(
            () => {
                return this._props.timelineVisible;
            },
            isVisible => {
                if (isVisible) {
                    this.regions
                        .filter(region => !region.hasData)
                        .forEach(region => {
                            for (let i = 0; i < 4; i++) {
                                this.copyDocDataToKeyFrame(DocListCast(region.keyframes)[i]);
                                if (i === 0 || i === 3) {
                                    //manually inputing fades
                                    DocListCast(region.keyframes)[i].opacity = 0.1;
                                }
                            }
                        });
                } else {
                    //this.revertState();
                }
            }
        );
    };

    @observable private saveStateKf: Doc | undefined = undefined;
    @observable private saveStateRegion: Doc | undefined = undefined;

    /**w
     * when scrubber position changes. Need to edit the logic
     */
    @action
    timeChange = () => {
        if (this.saveStateKf !== undefined || this._newKeyframe) {
            this.saveKeyframe();
        }
        const regiondata = this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on
        if (regiondata) {
            const leftkf: Doc | undefined = RegionHelpers.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists
            const rightkf: Doc | undefined = RegionHelpers.calcMinRight(regiondata, this.time); //right keyframe, if it exists
            const currentkf: Doc | undefined = this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe
            if (currentkf) {
                this.applyKeys(currentkf);
                runInAction(() => {
                    this.saveStateKf = currentkf;
                    this.saveStateRegion = regiondata;
                });
            } else if (leftkf && rightkf) {
                this.interpolate(leftkf, rightkf);
            }
        }
    };

    /**
     * applying changes (when saving the keyframe)
     * need to change the logic here
     */
    @action
    private applyKeys = (kf: Doc) => {
        this.primitiveWhitelist.forEach(key => {
            if (key === 'opacity' && this._props.animatedDoc === this._props.collection) {
                return;
            }
            if (!kf[key]) {
                this._props.animatedDoc[key] = undefined;
            } else {
                const stored = kf[key];
                this._props.animatedDoc[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
            }
        });
    };

    /**
     *  calculating current keyframe, if the scrubber is right on the keyframe
     */
    @action
    calcCurrent = (region: Doc) => {
        let currentkf: Doc | undefined = undefined;
        const keyframes = DocListCast(region.keyframes!);
        keyframes.forEach(kf => {
            if (NumCast(kf.time) === Math.round(this.time)) currentkf = kf;
        });
        return currentkf;
    };

    /**
     * basic linear interpolation function
     */
    @action
    interpolate = (left: Doc, right: Doc) => {
        this.primitiveWhitelist.forEach(key => {
            if (key === 'opacity' && this._props.animatedDoc === this._props.collection) {
                return;
            }
            if (typeof left[key] === 'number' && typeof right[key] === 'number') {
                //if it is number, interpolate
                const dif = NumCast(right[key]) - NumCast(left[key]);
                const deltaLeft = this.time - NumCast(left.time);
                const ratio = deltaLeft / (NumCast(right.time) - NumCast(left.time));
                this._props.animatedDoc[key] = NumCast(left[key]) + dif * ratio;
            } else {
                // case data
                const stored = left[key];
                this._props.animatedDoc[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
            }
        });
    };

    /**
     * finds region that corresponds to specific time (is there a region at this time?)
     * linear O(n) (maybe possible to optimize this with other Data structures?)
     */
    findRegion = (time: number) => {
        return this.regions?.find(rd => time >= NumCast(rd.position) && time <= NumCast(rd.position) + NumCast(rd.duration));
    };

    /**
     * double click on track. Signalling keyframe creation.
     */
    @action
    onInnerDoubleClick = (e: React.MouseEvent) => {
        const inner = this._inner.current!;
        const offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this._props.transform.Scale);
        this.createRegion(RegionHelpers.convertPixelTime(offsetX, 'mili', 'time', this._props.tickSpacing, this._props.tickIncrement));
    };

    /**
     * creates a region (KEYFRAME.TSX stuff).
     */
    @action
    createRegion = (time: number) => {
        if (this.findRegion(time) === undefined) {
            //check if there is a region where double clicking (prevents phantom regions)
            const regiondata = RegionHelpers.defaultKeyframe(); //create keyframe data

            regiondata.position = time; //set position
            const rightRegion = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.right, regiondata, this.regions);

            if (rightRegion && rightRegion.position - regiondata.position <= 4000) {
                //edge case when there is less than default 4000 duration space between this and right region
                regiondata.duration = rightRegion.position - regiondata.position;
            }
            if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) {
                Cast(this._props.animatedDoc.regions, listSpec(Doc))?.push(regiondata);
                this._newKeyframe = true;
                this.saveStateRegion = regiondata;
                return regiondata;
            }
        }
    };

    @action
    makeKeyData = (regiondata: RegionData, time: number, type: RegionHelpers.KeyframeType = RegionHelpers.KeyframeType.default) => {
        //Kfpos is mouse offsetX, representing time
        const trackKeyFrames = DocListCast(regiondata.keyframes);
        const existingkf = trackKeyFrames.find(TK => TK.time === time);
        if (existingkf) return existingkf;
        //else creates a new doc.
        const newKeyFrame: Doc = new Doc();
        newKeyFrame.time = time;
        newKeyFrame.type = type;
        this.copyDocDataToKeyFrame(newKeyFrame);
        //assuming there are already keyframes (for keeping keyframes in order, sorted by time)
        if (trackKeyFrames.length === 0) regiondata.keyframes!.push(newKeyFrame);
        trackKeyFrames
            .map(kf => NumCast(kf.time))
            .forEach((kfTime, index) => {
                if ((kfTime < time && index === trackKeyFrames.length - 1) || (kfTime < time && time < NumCast(trackKeyFrames[index + 1].time))) {
                    regiondata.keyframes!.splice(index + 1, 0, newKeyFrame);
                }
            });
        return newKeyFrame;
    };

    @action
    copyDocDataToKeyFrame = (doc: Doc) => {
        var somethingChanged = false;
        this.primitiveWhitelist.map(key => {
            const originalVal = this._props.animatedDoc[key];
            somethingChanged = somethingChanged || originalVal !== doc[key];
            if (doc.type === RegionHelpers.KeyframeType.end && key === 'opacity') doc.opacity = 0;
            else doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal;
        });
        return somethingChanged;
    };

    /**
     * UI sstuff here. Not really much to change
     */
    render() {
        const saveStateKf = this.saveStateKf;
        return (
            <div className="track-container">
                <div className="track">
                    <div
                        className="inner"
                        ref={this._inner}
                        style={{ height: `${this._trackHeight}px` }}
                        onDoubleClick={this.onInnerDoubleClick}
                        onPointerOver={() => Doc.BrushDoc(this._props.animatedDoc)}
                        onPointerOut={() => Doc.UnBrushDoc(this._props.animatedDoc)}>
                        {this.regions?.map((region, i) => {
                            return <Region key={`${i}`} {...this._props} saveStateKf={saveStateKf} RegionData={region} makeKeyData={this.makeKeyData} />;
                        })}
                    </div>
                </div>
            </div>
        );
    }
}
