import * as THREE from 'three';

import invariant from 'fbjs/lib/invariant';
import warning from 'fbjs/lib/warning';
import PropTypes from 'react/lib/ReactPropTypes';

import THREEElementDescriptor from '../THREEElementDescriptor';
import resource from '../decorators/resource';
import ResourceReference from '../../Resources/ResourceReference';
import propTypeInstanceOf from '../../utils/propTypeInstanceOf';

@resource
class MaterialDescriptorBase extends THREEElementDescriptor {
  constructor(react3Instance) {
    super(react3Instance);

    this.hasProp('slot', {
      type: PropTypes.string,
      updateInitial: true,
      update: (threeObject, slot, hasProperty) => {
        if (hasProperty) {
          threeObject.userData._materialSlot = slot;
        } else {
          threeObject.userData._materialSlot = 'material';
        }
      },
      default: 'material',
    });

    this.hasProp('transparent', {
      type: PropTypes.bool,
      simple: true,
    });

    this.hasProp('alphaTest', {
      type: PropTypes.number,
      updateInitial: true,
      update: (threeObject, alphaTest) => {
        threeObject.alphaTest = alphaTest;
        threeObject.needsUpdate = true;
      },
      default: 0,
    });

    this.hasProp('side', {
      type: PropTypes.oneOf([THREE.FrontSide, THREE.BackSide, THREE.DoubleSide]),
      updateInitial: true,
      update: (threeObject, side) => {
        threeObject.side = side;
      },
      default: THREE.FrontSide,
    });

    this.hasProp('depthTest', {
      type: PropTypes.bool,
      simple: true,
      default: true,
    });

    this.hasProp('depthWrite', {
      type: PropTypes.bool,
      simple: true,
      default: true,
    });

    this.hasProp('blending', {
      type: PropTypes.oneOf([
        THREE.NoBlending,
        THREE.NormalBlending,
        THREE.AdditiveBlending,
        THREE.SubtractiveBlending,
        THREE.MultiplyBlending,
        THREE.CustomBlending,
      ]),
      simple: true,
      default: THREE.NormalBlending,
    });

    this.hasProp('depthFunc', {
      type: PropTypes.oneOf([
        THREE.NeverDepth,
        THREE.AlwaysDepth,
        THREE.LessDepth,
        THREE.LessEqualDepth,
        THREE.EqualDepth,
        THREE.GreaterEqualDepth,
        THREE.GreaterDepth,
        THREE.NotEqualDepth,
      ]),
      simple: true,
      default: THREE.LessEqualDepth,
    });

    this.hasProp('opacity', {
      type: PropTypes.number,
      simple: true,
    });

    this.hasProp('visible', {
      type: PropTypes.bool,
      simple: true,
      default: true,
    });

    this.hasProp('vertexColors', {
      type: PropTypes.oneOf([
        THREE.NoColors,
        THREE.FaceColors,
        THREE.VertexColors,
      ]),
      simple: true,
      default: THREE.NoColors,
    });

    this._colors = [];
    this._supportedMaps = {};
  }

  hasMap(mapPropertyName = 'map') {
    this._supportedMaps[mapPropertyName] = true;

    this.hasProp(mapPropertyName, {
      type: propTypeInstanceOf(THREE.Texture),
      update(threeObject, value) {
        threeObject.userData[`_${mapPropertyName}}Property`] = value;

        if (!threeObject.userData[`_has${mapPropertyName}}TextureChild`]) {
          if (threeObject[mapPropertyName] !== value) {
            threeObject.needsUpdate = true;
          }
          threeObject[mapPropertyName] = value;
        } else {
          let slotInfo = 'texture';

          if (mapPropertyName !== 'map') {
            slotInfo += `with a '${mapPropertyName}' slot`;
          }

          warning(value === null, 'The material already has a' +
            ` ${slotInfo} assigned to it as a child;` +
            ` therefore the '${mapPropertyName}' property will have no effect`);
        }
      },
      updateInitial: true,
      default: null,
    });
  }

  getMaterialDescription(props) {
    const materialDescription = {};

    this._colors.forEach((colorPropName) => {
      if (props.hasOwnProperty(colorPropName)) {
        materialDescription[colorPropName] = props[colorPropName];
      }
    });

    if (props.hasOwnProperty('side')) {
      materialDescription.side = props.side;
    }

    return materialDescription;
  }

  hasColor(propName = 'color', defaultVal = 0xffffff) {
    if (process.env.NODE_ENV !== 'production') {
      invariant(this._colors.indexOf(propName) === -1,
        'This color is already defined for %s.',
        this.constructor.name);
    }

    this._colors.push(propName);

    this.hasProp(propName, {
      type: PropTypes.oneOfType([
        propTypeInstanceOf(THREE.Color),
        PropTypes.number,
        PropTypes.string,
      ]),
      update: (threeObject, value) => {
        threeObject[propName].set(value);
      },
      default: defaultVal,
    });
  }

  hasWireframe() {
    this.hasProp('wireframe', {
      type: PropTypes.bool,
      simple: true,
      default: false,
    });

    this.hasProp('wireframeLinewidth', {
      type: PropTypes.number,
      simple: true,
      default: 1,
    });
  }

  construct() {
    return new THREE.Material({});
  }

  applyInitialProps(threeObject, props) {
    threeObject.userData = {
      ...threeObject.userData,
      _hasTextureChild: false,
    };

    super.applyInitialProps(threeObject, props);
  }

  setParent(material, parentObject3D) {
    invariant(parentObject3D instanceof THREE.Mesh
      || parentObject3D instanceof THREE.Points
      || parentObject3D instanceof THREE.Sprite
      || parentObject3D instanceof THREE.Line, 'Parent is not a mesh');
    invariant(parentObject3D[material.userData._materialSlot] === undefined
      || parentObject3D[material.userData._materialSlot] === null,
      `Parent already has a ${material.userData._materialSlot} defined`);
    super.setParent(material, parentObject3D);

    parentObject3D[material.userData._materialSlot] = material;
  }

  unmount(material) {
    const parent = material.userData.markup.parentMarkup.threeObject;

    // could either be a resource description or an actual material
    if (parent instanceof THREE.Mesh ||
      parent instanceof THREE.Sprite ||
      parent instanceof THREE.Line ||
      parent instanceof THREE.Points) {
      const slot = material.userData._materialSlot;

      if (parent[slot] === material) {
        // TODO: set material slot to null rather than undefined

        parent[slot] = undefined;
      }
    }

    material.dispose();

    super.unmount(material);
  }

  highlight(threeObject) {
    const ownerMesh = threeObject.userData.markup.parentMarkup.threeObject;

    threeObject.userData.events.emit('highlight', {
      uuid: threeObject.uuid,
      boundingBoxFunc: () => {
        const boundingBox = new THREE.Box3();

        if (ownerMesh && ownerMesh.geometry && ownerMesh.geometry.computeBoundingBox) {
          ownerMesh.geometry.computeBoundingBox();
        }

        boundingBox.setFromObject(ownerMesh);

        return [boundingBox];
      },
    });
  }

  getBoundingBoxes(threeObject) {
    const boundingBox = new THREE.Box3();

    const ownerMesh = threeObject.userData.markup.parentMarkup.threeObject;

    if (ownerMesh && ownerMesh.geometry && ownerMesh.geometry.computeBoundingBox) {
      ownerMesh.geometry.computeBoundingBox();
    }

    boundingBox.setFromObject(ownerMesh);

    return [boundingBox];
  }

  hideHighlight(threeObject) {
    threeObject.userData.events.emit('hideHighlight');
  }

  addChildren(threeObject, children) {
    invariant(children.filter(this._invalidChild).length === 0,
      'Material children can only be textures or texture resource references!');
  }

  addChild(threeObject, child) {
    this.addChildren(threeObject, [child]);
  }

  moveChild() {
    // doesn't matter
  }

  removeChild() {
    // doesn't matter since the texture will take care of things on unmount
  }

  invalidChildInternal(child) {
    return !(child instanceof THREE.Texture
    || child instanceof ResourceReference);
  }

  _invalidChild = child => this.invalidChildInternal(child);
}

module.exports = MaterialDescriptorBase;
