import React, { Component } from "react";
import PropTypes from "prop-types";
import invariant from "invariant";
import getContext from "./getContext";

const __DEV__ = process.env.NODE_ENV === "development";

type WebGLContextAttributes = {
  alpha?: boolean;
  depth?: boolean;
  stencil?: boolean;
  antialias?: boolean;
  premultipliedAlpha?: boolean;
  preserveDrawingBuffer?: boolean;
  preferLowPowerToHighPerformance?: boolean;
  failIfMajorPerformanceCaveat?: boolean;
};

const propTypes: { [key: string]: any } = {
  onContextCreate: PropTypes.func.isRequired,
  onContextFailure: PropTypes.func.isRequired,
  onContextLost: PropTypes.func.isRequired,
  onContextRestored: PropTypes.func.isRequired,
  webglContextAttributes: PropTypes.object,
  width: PropTypes.number.isRequired,
  height: PropTypes.number.isRequired,
  style: PropTypes.object,
  pixelRatio: PropTypes.number,
  version: PropTypes.string,
};

class ErrorDebug extends Component<{ error: any }> {
  render() {
    const { error } = this.props;
    let title = String(error.rawError || error.message || error);
    let detail = String(error.longMessage || error.rawError || "");
    const style: any = {
      width: "100%",
      height: "100%",
      position: "absolute",
      top: 0,
      left: 0,
      padding: "1em",
      background: "#a00",
      color: "#fff",
      fontSize: "12px",
      lineHeight: "1.2em",
      fontStyle: "normal",
      fontWeight: "normal",
      fontFamily: "monospace",
      overflow: "auto",
    };
    const titleStyle = {
      fontWeight: "bold" as const,
      marginBottom: "1em",
    };
    const detailStyle = {
      whiteSpace: "pre" as const,
    };
    return (
      <div style={style}>
        <div style={titleStyle}>{title}</div>
        <div style={detailStyle}>{detail}</div>
      </div>
    );
  }
}

export default class GLViewDOM extends Component<
  {
    onContextCreate: (gl: WebGLRenderingContext) => void;
    onContextFailure: (e: Error) => void;
    onContextLost: () => void;
    onContextRestored: (gl: WebGLRenderingContext | null) => void;
    webglContextAttributes?: WebGLContextAttributes;
    pixelRatio?: number;
    width: number;
    height: number;
    style?: any;
    debug?: number;
    version?: string;
    [key: string]: any;
  },
  {
    error: Error | null;
  }
> {
  state: { error: Error | null } = {
    error: null,
  };
  static propTypes = propTypes;
  webglContextAttributes!: WebGLContextAttributes;
  canvas: HTMLCanvasElement | null = null;
  gl: WebGLRenderingContext | null = null;

  componentDidMount() {
    const { onContextCreate, onContextFailure } = this.props;
    const gl = this._createContext();
    if (gl) {
      this.gl = gl;
      onContextCreate(gl);
      const { canvas } = this;
      invariant(
        canvas,
        "canvas is not settled in GLViewDOM#componentDidMount"
      );
      canvas!.addEventListener("webglcontextlost", this._onContextLost);
      canvas!.addEventListener(
        "webglcontextrestored",
        this._onContextRestored
      );
    } else {
      onContextFailure(new Error("no-webgl-context"));
    }
  }

  componentWillUnmount() {
    if (this.gl) {
      this.gl = null;
    }
    const { canvas } = this;
    if (canvas) {
      canvas.removeEventListener("webglcontextlost", this._onContextLost);
      canvas.removeEventListener(
        "webglcontextrestored",
        this._onContextRestored
      );
    }
  }

  render() {
    const { error } = this.state;
    let {
      width,
      height,
      pixelRatio,
      style,
      debug,
      version,
      ...rest
    } = this.props;
    if (!pixelRatio)
      pixelRatio = Number(
        (typeof window === "object" && window.devicePixelRatio) || 1
      );
    for (let k in propTypes) {
      if (rest.hasOwnProperty(k)) {
        delete (rest as any)[k];
      }
    }
    return (
      <span
        style={{
          position: "relative",
          ...style,
          display: "inline-block",
          width,
          height,
        }}
      >
        <canvas
          ref={this.onRef}
          style={{ width, height }}
          width={width * pixelRatio}
          height={height * pixelRatio}
          {...rest}
        />
        {error ? <ErrorDebug error={error} /> : null}
      </span>
    );
  }

  _createContext() {
    const { webglContextAttributes, debug, version } = this.props;
    const gl = getContext(
      this.canvas!,
      debug
        ? { ...webglContextAttributes, preserveDrawingBuffer: true }
        : webglContextAttributes,
      (version || "auto") as "webgl" | "webgl2" | "auto"
    );
    this.webglContextAttributes = webglContextAttributes || {};
    return gl;
  }

  _onContextLost = (e: Event) => {
    e.preventDefault();
    this.gl = null;
    this.props.onContextLost();
  };

  _onContextRestored = () => {
    this.gl = this._createContext();
    this.props.onContextRestored(this.gl);
  };

  onRef = (ref: HTMLCanvasElement | null) => {
    this.canvas = ref;
  };

  debugError = !__DEV__
    ? null
    : (error: Error) => {
        this.setState({ error });
      };

  afterDraw = !__DEV__
    ? null
    : () => {
        if (this.state.error) {
          this.setState({ error: null });
        }
      };

  captureAsDataURL(...args: any[]): string {
    if (!this.webglContextAttributes.preserveDrawingBuffer) {
      console.warn(
        "Surface#captureAsDataURL is likely to not work if you don't define webglContextAttributes={{ preserveDrawingBuffer: true }}"
      );
    }
    invariant(this.canvas, "canvas is no longer available");
    return this.canvas!.toDataURL(...args);
  }

  captureAsBlob(...args: any[]): Promise<Blob> {
    if (!this.webglContextAttributes.preserveDrawingBuffer) {
      console.warn(
        "Surface#captureAsBlob is likely to not work if you don't define webglContextAttributes={{ preserveDrawingBuffer: true }}"
      );
    }
    return Promise.resolve().then(
      () =>
        new Promise((resolve, reject) =>
          this.canvas
            ? this.canvas.toBlob(resolve, ...args)
            : reject(new Error("canvas is no longer available"))
        )
    );
  }
}
