import { Color, LinearSRGBColorSpace, Object3D, SRGBColorSpace, Texture } from 'three';
import * as ThreeMeshUI from 'three-mesh-ui'
import type { Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior.js"

import { serializable } from '../../engine/engine_serialization_decorator.js';
import { ComponentInit } from '../../engine/engine_types.js';
import { NEEDLE_progressive } from '../../engine/extensions/NEEDLE_progressive.js';
import { RGBAColor } from "../../engine/js-extensions/index.js"
import { GameObject } from '../Component.js';
import { BaseUIComponent } from "./BaseUIComponent.js";
import { type IGraphic, type IRectTransformChangedReceiver } from './Interfaces.js';
import { Outline } from './Outline.js';
import { RectTransform } from './RectTransform.js';
import { onChange, scheduleAction } from "./Utils.js"

const _colorStateObject: { backgroundColor: Color, backgroundOpacity: number, borderColor: Color, borderOpacity: number } = {
    backgroundColor: new Color(1, 1, 1),
    backgroundOpacity: 1,
    borderColor: new Color(1, 1, 1),
    borderOpacity: 1,
};

/**
 * [Graphic](https://engine.needle.tools/docs/api/Graphic) provides basic rendering for UI elements with color, opacity, and texture support.
 * @category User Interface
 * @group Components
 */
export class Graphic extends BaseUIComponent implements IGraphic, IRectTransformChangedReceiver {

    get isGraphic() { return true; }

    @serializable(RGBAColor)
    get color(): RGBAColor {
        if (!this._color) this._color = new RGBAColor(1, 1, 1, 1);
        return this._color;
    }
    set color(col: RGBAColor) {
        const changed = !this._color || this._color.r !== col.r || this._color.g !== col.g || this._color.b !== col.b || this._color.alpha !== col.alpha;
        if (!changed) return;
        if (!this._color) {
            this._color = new RGBAColor(1, 1, 1, 1);
        }
        this._color.copy(col);
        this.onColorChanged();
    }

    private _alphaFactor: number = 1;
    setAlphaFactor(factor: number) {
        this._alphaFactor = factor;
        this.onColorChanged();
    }
    get alphaFactor() {
        return this._alphaFactor;
    }

    private sRGBColor: Color = new Color(1, 0, 1);
    protected onColorChanged() {
        if (this.uiObject) {
            this.sRGBColor.copy(this._color);
            this.sRGBColor.convertLinearToSRGB();
            _colorStateObject.backgroundColor = this.sRGBColor;
            _colorStateObject.backgroundOpacity = this._color.alpha;

            // E.g. when a button is setting states, we need to merge the state color with the base color
            const activeStateName = this.uiObject["_simpleState__activeStates"]?.[0];
            if (activeStateName) {
                const active = this.uiObject["_simpleState__states"]?.[activeStateName];
                if (active) {
                    if ("backgroundColor" in active) _colorStateObject.backgroundColor = active["backgroundColor"];
                    if ("backgroundOpacity" in active) _colorStateObject.backgroundOpacity = active["backgroundOpacity"];
                }
            }

            _colorStateObject.backgroundOpacity *= this._alphaFactor;
            this.applyEffects(_colorStateObject, this._alphaFactor);
            this.uiObject.set(_colorStateObject);
            this.markDirty();
        }
    }

    // used via animations
    private get m_Color() {
        return this._color;
    }

    @serializable()
    raycastTarget: boolean = true;

    protected uiObject: ThreeMeshUI.Block | null = null;
    private _color: RGBAColor = null!;

    private _rect: RectTransform | null = null;

    private _stateManager: SimpleStateBehavior | null = null;

    protected get rectTransform(): RectTransform {
        if (!this._rect) {
            this._rect = GameObject.getComponent(this.gameObject, RectTransform);
        }
        if (!this._rect) throw new Error("Not Supported: Make sure to add a RectTransform component before adding a UI Graphic component.");
        return this._rect!;
    }

    onParentRectTransformChanged() {
        this.uiObject?.set({ width: this.rectTransform.width, height: this.rectTransform.height })
        this.markDirty();
    }

    __internalNewInstanceCreated(init: ComponentInit<this>): this {
        super.__internalNewInstanceCreated(init);
        this._rect = null;
        this.uiObject = null;
        this._stateManager = null;
        if (this._color) this._color = this._color.clone();
        return this;
    }

    setState(state: string) {
        this.makePanel();
        if (this.uiObject) {
            //@ts-ignore
            this.uiObject.setState(state);
            this?.markDirty();
        }
    }

    setupState(state: object) {
        this.makePanel();
        if (this.uiObject) {
            // @marwie : v7.x now have a concurrent state management in core mimicking html/css
            // ie : (::firstChild::hover::disabled) where firstchild, hover and disabled are all on different channels
            // In order to keep needle Raycaster and EventSystem intact, I added in v7 a SimpleStateBehavior, which acts as previously

            if (!this._stateManager) this._stateManager = new SimpleStateBehavior(this.uiObject);
            //@ts-ignore
            this.uiObject.setupState(state.state, state.attributes);
        }
    }

    setOptions(opts: Options) {
        this.makePanel();
        if (this.uiObject) {
            //@ts-ignore
            this.uiObject.set(opts);
            // if (opts["backgroundColor"] !== undefined || opts["backgroundOpacity"] !== undefined)
            //     this.uiObject["updateBackgroundMaterial"]?.call(this.uiObject);
        }
    }

    awake() {
        super.awake();
        this.makePanel();

        // when _color is written to
        onChange(this, "_color", () => scheduleAction(this, this.onColorChanged));
    }

    onEnable(): void {
        super.onEnable();
        if (this.uiObject) {
            this.rectTransform.shadowComponent?.add(this.uiObject as unknown as Object3D);
            this.addShadowComponent(this.uiObject, this.rectTransform);
        }

    }

    onDisable(): void {
        super.onDisable();
        if (this.uiObject)
            this.removeShadowComponent();
    }

    private _currentlyCreatingPanel: boolean = false;
    protected makePanel() {
        if (this.uiObject) return;
        if (this._currentlyCreatingPanel) return;
        this._currentlyCreatingPanel = true;

        const offset = .015;
        // if (this.Root) offset = .02 * (1 / this.Root.gameObject.scale.z);
        const opts = {
            backgroundColor: this.color,
            backgroundOpacity: this.color.alpha,
            offset: offset, // without a tiny offset we get z fighting
        };
        this.onBeforeCreate(opts);
        this.applyEffects(opts);
        this.onCreate(opts);
        this.controlsChildLayout = false;
        this._currentlyCreatingPanel = false;
        this.onAfterCreated();

        this.onColorChanged();
    }

    protected onBeforeCreate(_opts: any) { }

    protected onCreate(opts: any) {
        this.uiObject = this.rectTransform.createNewBlock(opts);
        this.uiObject.name = this.name;
    }
    protected onAfterCreated() { }

    private applyEffects(opts, alpha: number = 1) {
        const outline = this.gameObject?.getComponent(Outline);
        if (outline) {
            if (outline.effectDistance) opts.borderWidth = Math.max(Math.abs(outline.effectDistance.x), Math.abs(outline.effectDistance.y));
            if (outline.effectColor) {
                opts.borderColor = outline.effectColor;
                opts.borderOpacity = outline.effectColor.alpha * alpha;
            }
        }
    }

    /** used internally to ensure textures assigned to UI use linear encoding */
    static textureCache: Map<Texture, Texture> = new Map();

    protected async setTexture(tex: Texture | null | undefined) {
        this.setOptions({ backgroundOpacity: 0 });
        if (tex) {
            // workaround for https://github.com/needle-tools/needle-engine-support/issues/109
            // if (tex.colorSpace === SRGBColorSpace || !tex.colorSpace || true) {
            if (Graphic.textureCache.has(tex)) {
                tex = Graphic.textureCache.get(tex)!;
            } else {
                if (tex.isRenderTargetTexture) {
                    // we can not clone the texture if it's a render target
                    // otherwise it won't be updated anymore in the UI
                    // TODO: below maskable graphic is flipped but settings a rendertexture results in the texture being upside down. 
                    // we should remove the flip below (scale.y *= -1) but this needs to be tested with all UI components
                }
                else {
                    const clone = tex.clone();
                    clone.colorSpace = LinearSRGBColorSpace;
                    Graphic.textureCache.set(tex, clone);
                    tex = clone;
                }
            }
            // }
            this.setOptions({ backgroundImage: tex, borderRadius: 0, backgroundOpacity: this.color.alpha, backgroundSize: "stretch" });
            NEEDLE_progressive.assignTextureLOD(tex, 0).then(res => {
                if (res instanceof Texture) {
                    if (tex) Graphic.textureCache.set(tex, res);
                    this.setOptions({ backgroundImage: res });
                    this.markDirty();
                }
            });
        }
        else {
            this.setOptions({ backgroundImage: undefined, borderRadius: 0, backgroundOpacity: this.color.alpha });
        }
        this.markDirty();
    }

    protected onAfterAddedToScene(): void {
        super.onAfterAddedToScene();
        if (this.shadowComponent) {
            // @TODO: I think we dont even need this anymore and this leads to the offset being applied twice
            //@ts-ignore
            this.shadowComponent.offset = this.shadowComponent.position.z;

            // console.log(this.shadowComponent);
            // setTimeout(()=>{
            //     this.shadowComponent?.traverse(c => {
            //         console.log(c);
            //         if(c.material) c.material.depthTest = false;
            //     });
            // },1000);
        }
    }
}

/**
 * @category User Interface
 * @group Components
 */
export class MaskableGraphic extends Graphic {

    private _flippedObject = false;

    protected onAfterCreated() {
        // flip image
        if (this.uiObject && !this._flippedObject) {
            this._flippedObject = true;
            this.uiObject.scale.y *= -1;
        }
    }
}