import {
    BufferGeometry,
    Camera,
    Color,
    IUniform,
    Material,
    MaterialEventMap,
    MaterialParameters,
    Object3D,
    Scene,
    Shader,
    WebGLRenderer,
} from 'three'
import type {IDisposable, IJSONSerializable} from 'ts-browser-helpers'
import type {MaterialExtension} from '../materials'
import type {ChangeEvent, IUiConfigContainer} from 'uiconfig.js'
import type {SerializationMetaType} from '../utils'
import type {IObject3D} from './IObject'
import {ISetDirtyCommonOptions} from './IObject'
import type {ITexture} from './ITexture'
import type {IImportResultUserData} from '../assetmanager'

export type IMaterialParameters = MaterialParameters & {customMaterialExtensions?: MaterialExtension[]}
// export type IMaterialEventTypes = 'dispose' | 'materialUpdate' | 'beforeRender' | 'beforeCompile' | 'afterRender' | 'textureUpdate' | 'beforeDeserialize'
// export type IMaterialEvent<T extends string = IMaterialEventTypes> = Event & {
//     type: T
//     bubbleToObject?: boolean
//     bubbleToParent?: boolean
//     material?: IMaterial
//
//     texture?: ITexture
//     oldTexture?: ITexture
//
//     uiChangeEvent?: ChangeEvent
// }

export interface IMaterialEventMap extends MaterialEventMap{
    beforeCompile: {
        shader: Shader
        renderer: WebGLRenderer
    }
    beforeRender: {
        renderer: WebGLRenderer
        scene: Scene
        camera: Camera
        geometry: BufferGeometry
        object: Object3D
    }
    afterRender: {
        renderer: WebGLRenderer
        scene: Scene
        camera: Camera
        geometry: BufferGeometry
        object: Object3D
    }
    /**
     * For internal use
     */
    beforeDeserialize: {
        data: unknown
        meta?: SerializationMetaType
        bubbleToObject: boolean
        bubbleToParent: boolean
    }
}

declare module 'three'{
    export interface MaterialEventMap{
        materialUpdate: {
            bubbleToObject?: boolean
            bubbleToParent?: boolean
            uiChangeEvent?: ChangeEvent
        } & IMaterialSetDirtyOptions
        textureUpdate: {
            texture: ITexture
            bubbleToObject?: boolean
            bubbleToParent?: boolean
            uiChangeEvent?: ChangeEvent
        }
    }
}

export interface IMaterialSetDirtyOptions extends ISetDirtyCommonOptions{
    /**
     * @default true
     */
    bubbleToObject?: boolean,
    /**
     * @default true
     */
    needsUpdate?: boolean,

    [key: string]: any
}
export interface IMaterialUserData extends IImportResultUserData{
    uuid?: string // adding to userdata also, so that its saved in gltf
    /**
     * Automatically dispose material when not used by any object in the scene
     * @default true
     */
    disposeOnIdle?: boolean

    renderToGBuffer?: boolean
    /**
     * Same as {@link renderToGBuffer} but for depth only, not normal or flags etc
     */
    renderToDepth?: boolean

    /**
     * Flag to tell the scene to prefer `material.envMapIntensity` over `scene.envMapIntensity`
     * only for materials that have envMapIntensity
     */
    separateEnvMapIntensity?: boolean // default: false
    /**
     * The environment map to use in the `RootScene`. To use this, object with the material must be in the RootScene, and the key should exist in the `RootScene`'s `textureSlots`.
     *
     * only for materials that have envMap
     */
    envMapSlotKey?: string

    cloneId?: string
    cloneCount?: number

    __envIntensity?: number // temp storage for envMapIntensity while rendering
    __isVariation?: boolean

    inverseAlphaMap?: boolean // only for physical material right now

    /**
     * See {@link MaterialManager.dispose} as {@link BaseGroundPlugin._refreshMaterial}
     */
    runtimeMaterial?: boolean

    /**
     * See {@link GBufferPlugin}
     */
    gBufferData?: {
        materialId?: number
        /**
         * @default true
         */
        tonemapEnabled?: boolean

        [key: string]: any
    }

    /**
     * Force a depth value in GBuffer.
     * This is useful to force center values like 0 to the depth.
     */
    forcedLinearDepth?: number // todo uiconfig for this in imaterial?

    /**
     * General flag to disable multiple plugins on the material at once, like SSAO, SSR, Bloom etc.
     */
    pluginsDisabled?: boolean // todo uiconfig for this in imaterial?

    // todo: move these to respective plugins

    /**
     * For SSCSPlugin
     */
    sscsDisabled?: boolean
    /**
     * For SSRPlugin
     */
    ssreflDisabled?: boolean
    /**
     * For SSRPlugin
     */
    ssreflNonPhysical?: boolean

    [key: string]: any


    // legacy, to be removed
    /**
     * @deprecated
     */
    setDirty?: (options?: IMaterialSetDirtyOptions) => void
    /**
     * @deprecated Use {@link postTonemap.tonemapEnabled} instead. This is kept because used in old files.
     */
    postTonemap?: boolean
}

export interface IMaterial<TE extends IMaterialEventMap = IMaterialEventMap> extends Material<TE>, IJSONSerializable, IDisposable, IUiConfigContainer {
    constructor: {
        TYPE: string
        TypeSlug: string
        MaterialProperties?: Record<string, any>
        MaterialTemplate?: IMaterialTemplate
    }
    assetType: 'material'
    setDirty(options?: IMaterialSetDirtyOptions): void;

    // clone?: ()=> any;

    needsUpdate: boolean;


    // toJSON same as three.js Material.toJSON
    // toJSON(meta?: any): any;

    // copyProps should be just setValues
    setValues(parameters: Material|(MaterialParameters&{type?:string}), allowInvalidType?: boolean, clearCurrentUserData?: boolean): this;
    toJSON(meta?: SerializationMetaType, _internal?: boolean): any;
    fromJSON(json: any, meta?: SerializationMetaType, _internal?: boolean): this | null;

    extraUniformsToUpload: Record<string, IUniform>
    materialExtensions: MaterialExtension[]
    registerMaterialExtensions: (customMaterialExtensions: MaterialExtension[]) => void;
    unregisterMaterialExtensions: (customMaterialExtensions: MaterialExtension[]) => void;

    /**
     * Managed internally, do not change manually
     */
    generator?: IMaterialGenerator

    /**
     * Managed internally, do not change manually
     */
    appliedMeshes: Set<IObject3D>
    lastShader?: Shader

    // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation
    userData: IMaterialUserData

    /**
     * Disposes the material from the GPU.
     * Set force to false if not sure the material is used by any object in the scene.
     * // todo add check for visible in scene also? or is that overkill
     * @param force - when true, same as three.js dispose. when false, only disposes if disposeOnIdle not false and not used by any object in the scene. default: true
     */
    dispose(force?: boolean): void

    // optional from subclasses, added here for autocomplete
    flatShading?: boolean
    map?: ITexture | null
    alphaMap?: ITexture | null
    envMap?: ITexture | null
    envMapIntensity?: number
    aoMap?: ITexture | null
    lightMap?: ITexture | null
    normalMap?: ITexture | null
    bumpMap?: ITexture | null
    displacementMap?: ITexture | null
    aoMapIntensity?: number
    lightMapIntensity?: number
    roughnessMap?: ITexture | null
    metalnessMap?: ITexture | null
    roughness?: number
    metalness?: number
    transmissionMap?: ITexture | null
    transmission?: number

    color?: Color
    wireframe?: boolean

    linewidth?: number

    isRawShaderMaterial?: boolean
    isPhysicalMaterial?: boolean
    isUnlitMaterial?: boolean
    isGBufferMaterial?: boolean

    // [key: string]: any
}


export type IMaterialGenerator<T extends IMaterial = IMaterial> = (params: any)=>T

export interface IMaterialTemplate<T extends IMaterial = IMaterial, TP = any>{
    templateUUID?: string,
    name: string,
    typeSlug?: string,
    alias?: string[], // alternate names
    materialType: string,
    generator?: IMaterialGenerator<T>,

    params?: TP
}
