import { Quaternion, Vector2, Vector3, Vector4 } from "three";

import { isDevEnvironment, LogType, showBalloonMessage } from "./debug/index.js";
import { $isAssigningProperties } from "./engine_serialization_core.js";
import { type Constructor, type IComponent } from "./engine_types.js";
import { watchWrite } from "./engine_utils.js";


declare type setter = (v: any) => void;
declare type getter = () => any;

/**
 * Marks a field to trigger the `onValidate` callback when its value changes.  
 * Useful for reacting to property changes from the editor or at runtime.  
 *
 * Your component must implement `onValidate(property?: string)` to receive notifications.  
 *
 * @param set Optional custom setter called before the value is assigned
 * @param get Optional custom getter called when the value is read
 *
 * @example Basic usage
 * ```ts
 * export class MyComponent extends Behaviour {
 *   @serializable()
 *   @validate()
 *   color: Color = new Color(1, 0, 0);
 *
 *   onValidate(property?: string) {
 *     if (property === "color") {
 *       console.log("Color changed to:", this.color);
 *     }
 *   }
 * }
 * ```
 * @example With custom setter
 * ```ts
 * @validate(function(value) {
 *   console.log("Setting speed to", value);
 * })
 * speed: number = 1;
 * ```
 */
export const validate = function (set?: setter, get?: getter) {
    // "descriptor : undefined" prevents @validate() to be added to property getters or setters
    return function (target: IComponent | any, propertyKey: string, descriptor?: undefined) {
        createPropertyWrapper(target, propertyKey, descriptor, set, get);
    }
}


function createPropertyWrapper(target: IComponent | any, _propertyKey: string | { name: string }, descriptor?: PropertyDescriptor,
    set?: setter,
    get?: getter) {

    if (!get && !set && !target.onValidate) return;

    // this is not undefined when its a property getter or setter already and not just a field
    // we currently only support validation of fields
    if (descriptor !== undefined) {
        console.error("Invalid usage of validate decorator. Only fields can be validated.", target, _propertyKey, descriptor);
        showBalloonMessage("Invalid usage of validate decorator. Only fields can be validated. Property: " + _propertyKey, { type: LogType.Error });
        return;
    }

    let propertyKey: string = "";
    if (typeof _propertyKey === "string") propertyKey = _propertyKey;
    else propertyKey = _propertyKey.name;

    if (target.__internalAwake) {
        // this is the hidden key we save the original property to
        const $prop = Symbol(propertyKey);
        // save the original awake method
        // we need to delay decoration until the object has been created
        const awake = target.__internalAwake;
        target.__internalAwake = function () {

            if (!this.onValidate) {
                if (isDevEnvironment()) console.warn("Usage of @validate decorate detected but there is no onValidate method in your class: \"" + target.constructor?.name + "\"")
                return;
            }

            // only build wrapper once per type
            if (this[$prop] === undefined) {

                // make sure the field is initialized in a hidden property
                this[$prop] = this[propertyKey];

                // For complex types we need to watch the write operation (the underlying values)
                // Since the object itself doesnt change (normally)
                // This is relevant if we want to use @validate() on e.g. a Vector3 which is animated from an animationclip
                const _val = this[propertyKey];
                if (_val instanceof Vector2 ||
                    _val instanceof Vector3 ||
                    _val instanceof Vector4 ||
                    _val instanceof Quaternion) {
                    const vec = this[propertyKey];
                    const cb = () => { this.onValidate(propertyKey); }
                    watchWrite(vec, cb)
                }

                Object.defineProperty(this, propertyKey, {
                    set: function (v) {
                        if (this[$isAssigningProperties] === true) {
                            this[$prop] = v;
                        }
                        else {
                            set?.call(this, v);
                            const oldValue = this[$prop];
                            this[$prop] = v;
                            this.onValidate?.call(this, propertyKey, oldValue);
                        }
                    },
                    get: function () {
                        get?.call(this);
                        return this[$prop];
                    },
                });
            }

            // call the original awake method
            awake.call(this);
        };
    }
}




/** Experimental attribute  
 * Use to hook into another type's methods and run before the other methods run (similar to Harmony prefixes).  
 * Return false to prevent the original method from running.  
 */
export const prefix = function <T>(type: Constructor<T>) {
    return function (target: IComponent | any, _propertyKey: string | { name: string }, _PropertyDescriptor: PropertyDescriptor) {

        let propertyKey: string = "";
        if (typeof _propertyKey === "string") propertyKey = _propertyKey;
        else propertyKey = _propertyKey.name;

        const targetType = type.prototype;
        const originalProp = Object.getOwnPropertyDescriptor(targetType, propertyKey);
        if (!originalProp?.value) {
            console.warn("Can not apply prefix: type does not have method named", _propertyKey, type);
            return;
        }

        const originalValue = originalProp.value;
        const prefix = target[propertyKey];
        Object.defineProperty(targetType, propertyKey, {
            value: function (...args) {
                const res = prefix?.call(this, ...args);
                // If the prefix method is async we need to check if the user returned false
                // In which case we don't want to call the original method
                if (res instanceof Promise) {
                    res.then((r) => {
                        if (r === false) return;
                        return originalValue.call(this, ...args);
                    });
                    return;
                }
                if (res === false) return;
                return originalValue.call(this, ...args);
            },
        });
    }
}
