import { IGroupInputData, IUI, IEventListenerId, IPointData, ILeafList, IEditSize, IGroup, IObject, IAlign, IAxis, IFunction, IMatrix, IApp, ILeaferMode } from '@leafer-ui/interface'
import { Group, DataHelper, LeafList, RenderEvent, LeafHelper, Direction9, Plugin, isString, PropertyEvent, LeaferEvent, isArray } from '@leafer-ui/draw'
import { DragEvent, RotateEvent, ZoomEvent, MoveEvent, useModule } from '@leafer-ui/core'

import { IEditBox, IEditPoint, IEditor, IEditorConfig, IEditTool, IEditorScaleEvent, IInnerEditor, ISimulateElement } from '@leafer-in/interface'

import { EditSelect } from './display/EditSelect'
import { EditBox } from './display/EditBox'
import { EditMask } from './display/EditMask'

import { config } from './config'

import { onTarget, onHover } from './editor/target'
import { targetAttr, mergeConfigAttr } from './decorator/data'
import { EditorHelper } from './helper/EditorHelper'
import { simulate } from './editor/simulate'
import { EditToolCreator } from './tool/EditToolCreator'
import { InnerEditorEvent } from './event/InnerEditorEvent'
import { EditorGroupEvent } from './event/EditorGroupEvent'
import { SimulateElement } from './display/SimulateElement'
import { TransformTool } from './tool/TransformTool'

@useModule(TransformTool, ['editBox', 'editTool', 'emitEvent'])
export class Editor extends Group implements IEditor {

    public config: IEditorConfig

    @mergeConfigAttr()
    readonly mergeConfig: IEditorConfig
    readonly mergedConfig: IEditorConfig

    @targetAttr(onTarget)
    public target?: IUI | IUI[]

    @targetAttr(onHover)
    public hoverTarget?: IUI

    public dimTarget?: IGroup | IGroup[] // 需要淡化的容器

    // 列表

    public leafList: ILeafList = new LeafList() // from target
    public get list(): IUI[] { return this.leafList.list as IUI[] }
    public get dragHoverExclude(): IUI[] { return [this.editBox.rect] }
    public openedGroupList: ILeafList = new LeafList()

    // 状态

    public get editing(): boolean { return !!this.list.length }
    public innerEditing: boolean
    public get groupOpening(): boolean { return !!this.openedGroupList.length }
    public resizeDirection?: Direction9

    public get multiple(): boolean { return this.list.length > 1 }
    public get single(): boolean { return this.list.length === 1 }

    public get dragPoint(): IEditPoint { return this.editBox.dragPoint }

    public get dragging(): boolean { return this.editBox.dragging }
    public get gesturing(): boolean { return this.editBox.gesturing } // 手势操作元素中

    public get moving(): boolean { return this.editBox.moving }
    public get resizing(): boolean { return this.editBox.resizing }
    public get rotating(): boolean { return this.editBox.rotating }
    public get skewing(): boolean { return this.editBox.skewing }

    // 组件

    public get element() { return this.multiple ? this.simulateTarget : this.list[0] as ISimulateElement }
    public simulateTarget: ISimulateElement = new SimulateElement(this)

    public editBox: IEditBox = new EditBox(this)
    public get buttons() { return this.editBox.buttons }

    public editTool?: IEditTool

    public innerEditor?: IInnerEditor
    public editToolList: IObject = {}

    public selector: EditSelect = new EditSelect(this)
    public editMask: EditMask = new EditMask(this)

    public hasDimOthers?: boolean

    public get targetLeafer() { const first = this.list[0]; return first && first.leafer }
    public targetChanged: boolean
    public targetEventIds: IEventListenerId[] = []


    constructor(userConfig?: IEditorConfig, data?: IGroupInputData) {
        super(data)
        let mergedConfig: IEditorConfig = DataHelper.clone(config)
        if (userConfig) mergedConfig = DataHelper.default(userConfig, mergedConfig)
        this.mergedConfig = this.config = mergedConfig

        this.addMany(this.editMask, this.selector, this.editBox)
        if (!Plugin.has('resize')) this.config.editSize = 'scale'
    }

    // select 

    public select(target: IUI | IUI[]): void {
        this.target = target
    }

    public cancel(): void {
        this.target = null
    }

    // item

    public hasItem(item: IUI): boolean {
        return this.leafList.has(item)
    }

    public getItem(index?: number): IUI {
        return this.list[index || 0]
    }

    public addItem(item: IUI): void {
        if (!this.hasItem(item) && !item.locked) this.leafList.add(item), this.target = this.leafList.list as IUI[]
    }

    public removeItem(item: IUI): void {
        if (this.hasItem(item)) this.leafList.remove(item), this.target = this.leafList.list as IUI[]
    }

    public shiftItem(item: IUI): void {
        this.hasItem(item) ? this.removeItem(item) : this.addItem(item)
    }

    // 淡化 / 突出

    public setDimOthers(value: boolean | number, attrName: 'bright' | 'dim' = 'dim', list?: IUI[]): void {
        if (!list) {
            const { dimTarget, targetLeafer } = this
            list = dimTarget ? (isArray(dimTarget) ? dimTarget : [dimTarget]) : [targetLeafer]
        }
        if (list[0] && list[0][attrName] !== (value || false)) list.forEach(item => DataHelper.stintSet(item, attrName, value))
    }

    public setBright(value: boolean): void {
        this.setDimOthers(value, 'bright', this.list)
    }

    public cancelDimOthers(): void {
        this.setDimOthers(false)
        this.setBright(false)
        this.hasDimOthers = undefined
    }

    // update

    public update(): void {
        if (this.editing) {
            if (!this.element.parent) return this.cancel()
            if (this.innerEditing) this.innerEditor.update()
            this.editTool.update()
            this.selector.update()
        }
    }

    public updateEditBox(): void {
        if (this.multiple) simulate(this)
        this.update()
    }


    // editTool

    public getEditTool(name: string): IEditTool {
        return this.editToolList[name] = this.editToolList[name] || EditToolCreator.get(name, this)
    }

    public updateEditTool(): void {
        this.unloadEditTool()

        if (this.editing) {
            const target = this.element
            let name = target.editOuter || 'EditTool'

            const { beforeEditOuter } = this.mergeConfig
            if (beforeEditOuter) {
                const check = beforeEditOuter({ target, name })
                if (isString(check)) name = check
                else if (check === false) return
            }

            if (EditToolCreator.list[name]) {
                const tool = this.editTool = this.getEditTool(name)
                this.editBox.load()
                tool.load()
                this.update()
            }
        }
    }

    public unloadEditTool(): void {
        let tool = this.editTool
        if (tool) {
            this.editBox.unload()
            tool.unload()
            this.editTool = null
        }
    }


    // get

    public getEditSize(_ui: IUI): IEditSize {
        return this.mergeConfig.editSize
    }


    // TransformTool will rewrite -----

    // operate 

    public onMove(_e: DragEvent | MoveEvent): void { }

    public onScale(_e: DragEvent | ZoomEvent): void { }

    public onRotate(_e: DragEvent | RotateEvent): void { }

    public onSkew(_e: DragEvent): void { }


    // transform

    public move(_x: number | IPointData, _y = 0): void { }

    public scaleWithDrag(_data: IEditorScaleEvent): void { }

    override scaleOf(_origin: IPointData | IAlign, scaleX: number, _scaleY = scaleX, _resize?: boolean): void { }

    override flip(_axis: IAxis): void { }

    override rotateOf(_origin: IPointData | IAlign, _rotation: number): void { }

    override skewOf(_origin: IPointData | IAlign, _skewX: number, _skewY = 0, _resize?: boolean): void { }

    public checkTransform(_type: 'moveable' | 'resizeable' | 'rotateable' | 'skewable'): boolean { return undefined }

    protected getWorldOrigin(_origin: IPointData | IAlign): IPointData { return undefined }

    protected getChangedTransform(_func: IFunction): IMatrix { return undefined }

    // --------


    // group

    public group(userGroup?: IGroup | IGroupInputData): IGroup {
        if (this.multiple) {
            this.emitGroupEvent(EditorGroupEvent.BEFORE_GROUP)
            this.target = EditorHelper.group(this.list, this.element, userGroup)
            this.emitGroupEvent(EditorGroupEvent.GROUP, this.target as IGroup)
        }
        return this.target as IGroup
    }

    public ungroup(): IUI[] {
        const { list } = this
        if (list.length) {
            list.forEach(item => item.isBranch && this.emitGroupEvent(EditorGroupEvent.BEFORE_UNGROUP, item as IGroup))
            this.target = EditorHelper.ungroup(list)
            list.forEach(item => item.isBranch && this.emitGroupEvent(EditorGroupEvent.UNGROUP, item as IGroup))
        }
        return this.list
    }

    public openGroup(group: IGroup): void {
        this.emitGroupEvent(EditorGroupEvent.BEFORE_OPEN, group)
        this.openedGroupList.add(group)
        group.hitChildren = true
        this.emitGroupEvent(EditorGroupEvent.OPEN, group)
    }

    public closeGroup(group: IGroup): void {
        this.emitGroupEvent(EditorGroupEvent.BEFORE_CLOSE, group)
        this.openedGroupList.remove(group)
        group.hitChildren = false
        this.emitGroupEvent(EditorGroupEvent.CLOSE, group)
    }

    public checkOpenedGroups(): void {
        const opened = this.openedGroupList
        if (opened.length) {
            let { list } = opened
            if (this.editing) list = [], opened.forEach(item => this.list.every(leaf => !LeafHelper.hasParent(leaf, item)) && list.push(item))
            list.forEach(item => this.closeGroup(item as IGroup))
        }
        if (this.editing && !this.selector.dragging) this.checkDeepSelect()
    }

    public checkDeepSelect(): void {
        let parent: IGroup, { list } = this
        for (let i = 0; i < list.length; i++) {
            parent = list[i].parent
            while (parent && !parent.hitChildren) {
                this.openGroup(parent)
                parent = parent.parent
            }
        }
    }

    public emitGroupEvent(type: string, group?: IGroup): void {
        const event = new EditorGroupEvent(type, { editTarget: group })
        this.emitEvent(event)
        if (group) group.emitEvent(event)
    }

    // inner

    public getInnerEditor(name: string): IInnerEditor {
        return this.editToolList[name] = this.editToolList[name] || EditToolCreator.get(name, this)
    }

    public openInnerEditor(target?: IUI, nameOrSelect?: string | boolean, select?: boolean): void {
        let name: string
        if (isString(nameOrSelect)) name = nameOrSelect
        else if (!select) select = nameOrSelect

        if (target && select) this.target = target

        if (this.single) {
            if (!target) target = this.element
            if (!name) name = target.editInner

            const { beforeEditInner } = this.mergeConfig
            if (beforeEditInner) {
                const check = beforeEditInner({ target, name })
                if (isString(check)) name = check
                else if (check === false) return
            }

            if (EditToolCreator.list[name]) {
                this.editTool.unload()
                this.innerEditing = true
                this.innerEditor = this.getInnerEditor(name)
                this.innerEditor.editTarget = target

                this.emitInnerEvent(InnerEditorEvent.BEFORE_OPEN)
                this.innerEditor.load()
                this.emitInnerEvent(InnerEditorEvent.OPEN)
            }
        }
    }

    public closeInnerEditor(onlyInnerEditor?: boolean): void {
        if (this.innerEditing) {
            this.innerEditing = false

            this.emitInnerEvent(InnerEditorEvent.BEFORE_CLOSE)
            this.innerEditor.unload()
            this.emitInnerEvent(InnerEditorEvent.CLOSE)

            if (!onlyInnerEditor) this.updateEditTool()
            this.innerEditor = null
        }
    }

    public emitInnerEvent(type: string): void {
        const { innerEditor } = this, { editTarget } = innerEditor
        const event = new InnerEditorEvent(type, { editTarget, innerEditor })
        this.emitEvent(event)
        editTarget.emitEvent(event)
    }

    // lock

    public lock(): void {
        this.list.forEach(leaf => leaf.locked = true)
        this.update()
    }

    public unlock(): void {
        this.list.forEach(leaf => leaf.locked = false)
        this.update()
    }

    // level

    public toTop(): void {
        if (this.list.length) {
            EditorHelper.toTop(this.list)
            this.leafList.update()
        }
    }

    public toBottom(): void {
        if (this.list.length) {
            EditorHelper.toBottom(this.list)
            this.leafList.update()
        }
    }

    protected onAppRenderStart(app: IApp): void {
        if (this.targetChanged = app.children.some(leafer => leafer !== this.leafer && leafer.renderer.changed)) this.editBox.forceRender()
    }

    protected onRenderStart(): void {
        if (this.targetChanged) this.update()
    }

    protected onChildScroll(): void {
        if (this.multiple) this.updateEditBox()
    }

    // event 

    public listenTargetEvents(): void {
        if (!this.targetEventIds.length) {
            const { app, leafer, targetLeafer, editMask } = this
            this.targetEventIds = [
                leafer.on_(RenderEvent.START, this.onRenderStart, this),
                targetLeafer && targetLeafer.on_(PropertyEvent.SCROLL, this.onChildScroll, this),
                app.on_(RenderEvent.CHILD_START, this.onAppRenderStart, this),
                app.on_(LeaferEvent.UPDATE_MODE, (data: { mode: ILeaferMode }) => { if (data.mode && data.mode !== 'normal') this.cancel() })
            ]
            if (editMask.visible) editMask.forceRender()
        }
    }

    public removeTargetEvents(): void {
        const { targetEventIds, editMask } = this
        if (targetEventIds.length) {
            this.off_(targetEventIds)
            if (editMask.visible) editMask.forceRender()
        }
    }

    public destroy(): void {
        if (!this.destroyed) {
            this.target = this.hoverTarget = null

            Object.values(this.editToolList).forEach(item => item.destroy())
            this.simulateTarget.destroy()

            this.editToolList = {}
            this.simulateTarget = this.editTool = this.innerEditor = null

            super.destroy()
        }
    }

}