import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { OmitKeys } from '../../../ClientUtils';
import { numberRange } from '../../../Utils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { TransitionTimer } from '../../../fields/DocSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ComputedField } from '../../../fields/ScriptField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DocumentManager } from '../../util/DocumentManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
import { DocComponent } from '../DocComponent';
import { StyleProp } from '../StyleProvider';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import './CollectionFreeFormDocumentView.scss';
import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
import { FieldViewProps } from './FieldView';

/// Ugh, typescript has no run-time way of iterating through the keys of an interface.  so we need
/// manaully keep this list of keys in synch wih the fields of the freeFormProps interface
const freeFormPropsKeys = ['x', 'y', 'z', 'zIndex', 'rotation', 'opacity', 'backgroundColor', 'color', 'highlight', 'width', 'height', 'autoDim', 'transition'];
interface freeFormProps {
    x: number;
    y: number;
    z: number;
    width: number;
    height: number;
    zIndex?: number;
    autoDim?: number; // 1 means use Panel Width/Height, 0 means use width/height
    rotation?: number;
    color?: string;
    backgroundColor?: string;
    opacity?: number;
    highlight?: boolean;
    transition?: string;
}

export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
    RenderCutoffProvider: (doc: Doc) => boolean;
    CollectionFreeFormView: CollectionFreeFormView;
}
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps & freeFormProps>() {
    get displayName() { // this makes mobx trace() statements more descriptive
        return 'CollectionFreeFormDocumentView(' + this.Document.title + ')';
    } // prettier-ignore
    public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container';
    public static animFields: { key: string; val?: number }[] = [
        { key: 'x' },
        { key: 'y' },
        { key: 'opacity', val: 1 },
        { key: '_height' },
        { key: '_width' },
        { key: '_rotation', val: 0 },
        { key: '_layout_scrollTop' },
        { key: '_currentFrame' },
        { key: 'freeform_scale', val: 1 },
        { key: 'freeform_panX' },
        { key: 'freeform_panY' },
    ]; // fields that are configured to be animatable using animation frames
    public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
    public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
    public static from(dv?: DocumentView) {
        return dv?._props.parent instanceof CollectionFreeFormDocumentView ? dv._props.parent : undefined;
    }

    constructor(props: CollectionFreeFormDocumentViewProps & freeFormProps) {
        super(props);
        makeObservable(this);
    }

    get WrapperKeys() {
        // each of these keys is set by the CollectionView and passed via props. however, if any one of these props changes
        // (or any other prop), then it's as if they all change.
        // Anything that accesses these props will then invalidate unncessarily.
        // To avoid this, we copy these prop values into local observables.  Now when 'props' changes, nothing invalidates.
        // Instead, we copy each values into its observable which ohnly triggers invalidations if the new value is different
        // than the old value, and then only things that access that observable will invalidate.
        return freeFormPropsKeys
                  .map(key => ({upper:key[0].toUpperCase() + key.substring(1), lower:key})); // prettier-ignore
    }
    @observable X = this.props.x;
    @observable Y = this.props.y;
    @observable Z = this.props.z;
    @observable ZIndex = this.props.zIndex;
    @observable Rotation = this.props.rotation;
    @observable Opacity = this.props.opacity;
    @observable BackgroundColor = this.props.backgroundColor;
    @observable Color = this.props.color;
    @observable Highlight = this.props.highlight;
    @observable Width = this.props.width;
    @observable Height = this.props.height;
    @observable AutoDim = this.props.autoDim;
    @observable Transition = this.props.transition;

    componentDidMount(): void {
        if (this.props.transition && !this.Document[TransitionTimer]) {
            const num = Number(this.props.transition.match(/([0-9.]+)s/)?.[1]) * 1000 || Number(this.props.transition.match(/([0-9.]+)ms/)?.[1]);
            this.Document[TransitionTimer] = setTimeout(
                action(() => {
                    this.Document[TransitionTimer] = this.Transition = undefined;
                }),
                num
            );
        }
    }

    componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewProps & freeFormProps>>) {
        super.componentDidUpdate(prevProps);
        this.WrapperKeys.forEach(
            action(keys => {
                (this as any)[keys.upper] = (this.props as any)[keys.lower];
            })
        );
    }

    CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking
    // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes
    DataTransition = ()    => this.Transition || StrCast(this.Document.dataTransition); // prettier-ignore
    RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking
    PanelWidth = ()        => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore
    PanelHeight = ()       => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore

    styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => {
        if (doc === this.layoutDoc) {
            switch (property.split(':')[0]) {
                case StyleProp.Opacity:         return this.Opacity; // only change the opacity for this specific document, not its children
                case StyleProp.BackgroundColor: return this.BackgroundColor;
                case StyleProp.Color:           return this.Color;
                default:
            } // prettier-ignore
        }
        return this._props.styleProvider?.(doc, props, property);
    };

    public static getValues(doc: Doc, time: number, fillIn: boolean = true) {
        return CollectionFreeFormDocumentView.animFields.reduce(
            (p, val) => {
                p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce(
                    (prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev),
                    undefined as any as number
                );
                return p;
            },
            {} as { [val: string]: Opt<number> }
        );
    }

    public static getStringValues(doc: Doc, time: number) {
        return CollectionFreeFormDocumentView.animStringFields.reduce(
            (p, val) => {
                p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev), undefined as any as string);
                return p;
            },
            {} as { [val: string]: Opt<string> }
        );
    }

    public static setStringValues(time: number, d: Doc, vals: { [val: string]: Opt<string> }) {
        const timecode = Math.round(time);
        Object.keys(vals).forEach(val => {
            const findexed = Cast(d[`${val}_indexed`], listSpec('string'), []).slice();
            findexed[timecode] = vals[val] || '';
            d[`${val}_indexed`] = new List<string>(findexed);
        });
    }

    public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) {
        const timecode = Math.round(time);
        Object.keys(vals).forEach(val => {
            const findexed = Cast(d[`${val}_indexed`], listSpec('number'), []).slice();
            findexed[timecode] = vals[val] || 0;
            d[`${val}_indexed`] = new List<number>(findexed);
        });
    }
    public static gotoKeyFrame(doc: Doc, newFrame: number) {
        if (doc) {
            const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]);
            const currentFrame = Cast(doc._currentFrame, 'number', null);
            if (currentFrame === undefined) {
                doc._currentFrame = 0;
                CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
            }
            CollectionFreeFormView.updateKeyframe(undefined, [...childDocs, doc], currentFrame || 0);
            doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
        }
    }

    public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) {
        docs.forEach(doc => {
            if (doc.appearFrame === undefined) doc.appearFrame = currTimecode;
            if (!doc.opacity_indexed) {
                // opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in
                doc.opacity_indexed = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)));
            }
            CollectionFreeFormDocumentView.animFields.forEach(val => {
                doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val);
            });
            CollectionFreeFormDocumentView.animStringFields.forEach(val => {
                doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode);
            });
            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
                doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode);
            });
            const targetDoc = doc; // data fields, like rtf 'text' exist on the data doc, so
            // doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
            targetDoc.activeFrame = ComputedField.MakeFunction('this.embedContainer?._currentFrame||0');
            targetDoc.dataTransition = 'inherit';
        });
    }

    float = () => {
        const topDoc = this.Document;
        const containerDocView = this._props.containerViewPath?.().lastElement();
        const screenXf = containerDocView?.screenToContentsTransform();
        if (screenXf) {
            SelectionManager.DeselectAll();
            if (topDoc.z) {
                const spt = screenXf.inverse().transformPoint(NumCast(topDoc.x), NumCast(topDoc.y));
                topDoc.z = 0;
                [topDoc.x, topDoc.y] = spt;
                this._props.removeDocument?.(topDoc);
                this._props.addDocTab(topDoc, OpenWhere.inParentFromScreen);
            } else {
                const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0);
                const fpt = screenXf.transformPoint(spt[0], spt[1]);
                topDoc.z = 1;
                [topDoc.x, topDoc.y] = fpt;
            }
            setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, containerDocView), false), 0);
        }
    };

    nudge = (x: number, y: number) => {
        const [locX, locY] = this._props.ScreenToLocalTransform().transformDirection(x, y);
        this.Document.x = this.X + locX;
        this.Document.y = this.Y + locY;
    };
    screenToLocalTransform = () =>
        this._props
            .ScreenToLocalTransform()
            .translate(-this.X, -this.Y)
            .rotateDeg(-(this.Rotation || 0));
    returnThis = () => this;

    /// this indicates whether the doc view is activated because of its relationshop to a group
    //  'group' - this is a group that is activated because it's on an active canvas, but is not part of some other group
    //  'child' - this is a group child that is activated because its containing group is activated
    //  'inactive' - this is a group child but it is not active
    //  undefined - this is not activated by a group
    isGroupActive = () => {
        if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined;
        const backColor = this.BackgroundColor;
        const isGroup = this.dataDoc.isGroup && (!backColor || backColor === 'transparent');
        return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined;
    };
    localRotation = () => this._props.rotation;
    render() {
        TraceMobx();

        return (
            <div
                className={CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName}
                style={{
                    width: this.PanelWidth(),
                    height: this.PanelHeight(),
                    transform: `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rotation)}deg)`,
                    transition: this.DataTransition(),
                    zIndex: this.ZIndex,
                    display: this.Width ? undefined : 'none',
                }}>
                {this.RenderCutoffProvider(this.Document) ? (
                    <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
                ) : (
                    <DocumentView
                        parent={this}
                        // eslint-disable-next-line react/jsx-props-no-spreading
                        {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
                        DataTransition={this.DataTransition}
                        LocalRotation={this.localRotation}
                        CollectionFreeFormDocumentView={this.returnThis}
                        styleProvider={this.styleProvider}
                        ScreenToLocalTransform={this.screenToLocalTransform}
                        isGroupActive={this.isGroupActive}
                        PanelWidth={this.PanelWidth}
                        PanelHeight={this.PanelHeight}
                    />
                )}
            </div>
        );
    }
}
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) {
    CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame);
});
