import { ComponentPropsOptions, ComponentPublicInstance, defineComponent, InjectionKey, PropType, watch } from 'vue'
import { Color, Material, MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, MeshPhysicalMaterial, MeshStandardMaterial, MeshToonMaterial, PointsMaterial as TPointsMaterial, Texture, ShadowMaterial as TShadowMaterial } from 'three'
import { MeshInjectionKey, MeshInterface } from '../meshes/Mesh'
import { bindObjectProp, propsValues } from '../tools'
import { BasicMaterialPropsInterface, LambertMaterialPropsInterface, MaterialPropsInterface, PhongMaterialPropsInterface, PhysicalMaterialPropsInterface, PointsMaterialPropsInterface, StandardMaterialPropsInterface, ToonMaterialPropsInterface } from './types'

export interface MaterialSetupInterface {
  mesh?: MeshInterface
  material?: Material
  createMaterial?(): Material
}

export interface MaterialInterface extends MaterialSetupInterface {
  setTexture(texture: Texture | null, key: string): void
}

export interface MaterialPublicInterface extends ComponentPublicInstance, MaterialInterface {}

export const MaterialInjectionKey: InjectionKey<MaterialPublicInterface> = Symbol('Material')

const BaseMaterial = defineComponent({
  emits: ['created'],
  props: {
    color: { type: String, default: '#ffffff' },
    props: { type: Object as PropType<MaterialPropsInterface>, default: () => ({}) },
  },
  inject: {
    mesh: MeshInjectionKey as symbol,
  },
  setup(): MaterialSetupInterface {
    return {}
  },
  provide() {
    return {
      [MaterialInjectionKey as symbol]: this,
    }
  },
  created() {
    if (!this.mesh) {
      console.error('Missing parent Mesh')
      return
    }

    if (this.createMaterial) {
      const material = this.material = this.createMaterial()
      // @ts-ignore
      watch(() => this.color, (value) => { material.color.set(value) })
      bindObjectProp(this, 'props', material, false, this.setProp)
      this.$emit('created', material)
      this.mesh.setMaterial(material)
    }
  },
  unmounted() {
    this.material?.dispose()
  },
  methods: {
    getMaterialParams() {
      return { ...propsValues(this.$props, ['props']), ...this.props }
    },
    setProp(material: any, key: string, value: any, needsUpdate = false) {
      const dstVal = material[key]
      if (dstVal instanceof Color) dstVal.set(value)
      else material[key] = value
      material.needsUpdate = needsUpdate
    },
    setTexture(texture: Texture | null, key = 'map') {
      this.setProp(this.material, key, texture, true)
    },
  },
  render() {
    return this.$slots.default ? this.$slots.default() : []
  },
  __hmrId: 'Material',
})

export default BaseMaterial

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function materialComponent<P extends Readonly<ComponentPropsOptions>>(
  name: string,
  props: P,
  createMaterial: {(opts: any): Material}
) {
  return defineComponent({
    name,
    extends: BaseMaterial,
    props,
    methods: {
      createMaterial() {
        return createMaterial(this.getMaterialParams())
      },
    },
  })
}

// TODO : proper
export const BasicMaterial = materialComponent('BasicMaterial', { props: { type: Object as PropType<BasicMaterialPropsInterface>, default: () => ({}) } }, (opts) => new MeshBasicMaterial(opts))
export const LambertMaterial = materialComponent('LambertMaterial', { props: { type: Object as PropType<LambertMaterialPropsInterface>, default: () => ({}) } }, (opts) => new MeshLambertMaterial(opts))
export const PhongMaterial = materialComponent('PhongMaterial', { props: { type: Object as PropType<PhongMaterialPropsInterface>, default: () => ({}) } }, (opts) => new MeshPhongMaterial(opts))
export const PhysicalMaterial = materialComponent('PhysicalMaterial', { props: { type: Object as PropType<PhysicalMaterialPropsInterface>, default: () => ({}) } }, (opts) => new MeshPhysicalMaterial(opts))
export const PointsMaterial = materialComponent('PointsMaterial', { props: { type: Object as PropType<PointsMaterialPropsInterface>, default: () => ({}) } }, (opts) => new TPointsMaterial(opts))
export const ShadowMaterial = materialComponent('ShadowMaterial', { color: { type: String, default: '#000000' }, props: { type: Object as PropType<MaterialPropsInterface>, default: () => ({}) } }, (opts) => new TShadowMaterial(opts))
export const StandardMaterial = materialComponent('StandardMaterial', { props: { type: Object as PropType<StandardMaterialPropsInterface>, default: () => ({}) } }, (opts) => new MeshStandardMaterial(opts))
export const ToonMaterial = materialComponent('ToonMaterial', { props: { type: Object as PropType<ToonMaterialPropsInterface>, default: () => ({}) } }, (opts) => new MeshToonMaterial(opts))