import React, { Component } from 'react';
import { Token } from '../core/token';
import { warning } from '../utils/helpers';
import { ValidationMap } from 'prop-types';
import { Dictionary } from 'infinitymint/dist/app/helpers';
import { KeyValue } from 'infinitymint/dist/app/interfaces';

export interface RenderClass
    extends Component<RenderComponentProps, RenderComponentState> {
    onWindowResize(shouldReload?: boolean): void;
    renderToken(): string;
    load(): Promise<void | string>;
    getDataURL(): string;
    getContainerId(): string;
}

export interface RenderProps extends Dictionary<any> {
    scaleFactor?: number;
}

export interface RenderComponentProps extends KeyValue {
    children?: React.ReactNode;
    token: Token;
    rendererProps?: RenderProps;
}

export interface RenderComponentState extends KeyValue {
    loaded?: boolean;
    seed?: string;
    rendererProps?: RenderProps;
    savedRender?: any;
}
/**
 * Extend this class to create a new renderer
 */
export abstract class RenderComponent
    extends Component<RenderComponentProps, RenderComponentState>
    implements RenderClass
{
    propTypes?: React.WeakValidationMap<any>;
    contextType?: React.Context<any>;
    contextTypes?: ValidationMap<any>;
    childContextTypes?: ValidationMap<any>;
    defaultProps?: Partial<any>;
    displayName?: string = 'RenderComponent';
    getDerivedStateFromProps?: React.GetDerivedStateFromProps<any, {}>;
    getDerivedStateFromError?: React.GetDerivedStateFromError<any, {}>;

    onWindowResize(shouldReload?: boolean): void {
        throw new Error('Method not implemented.');
    }
    renderToken(): string {
        throw new Error('Method not implemented.');
    }
    load(): Promise<string | void> {
        throw new Error('Method not implemented.');
    }
    getDataURL(): string {
        throw new Error('Method not implemented.');
    }
    getContainerId(): string {
        throw new Error('Method not implemented.');
    }
    /**
     * base64encoded image/png data url of the rendered image, created after load is called
     */
    protected renderedImage: string;
}

/**
 * Default Render class using the canvas element
 */
export default class DefaultRenderer extends RenderComponent {
    constructor(props: {
        children?: React.ReactNode;
        token: Token;
        rendererProps?: RenderProps;
    }) {
        super(props);

        this.state = {
            loaded: false,
            seed: btoa('IM_' + Math.floor(Math.random() * 1000)),
            rendererProps: props.rendererProps,
        };
    }

    /**
     *
     * @param shouldReload
     * @returns
     */
    onWindowResize(shouldReload = true) {
        let canvas = this.getCanvasElement();

        if (!canvas) return;

        let clientHeight =
            (Math.ceil(canvas.parentElement.clientWidth / 16) * 16) / 4;

        if (this.state.rendererProps?.scaleFactor)
            clientHeight *= this.state.rendererProps.scaleFactor;

        canvas.width = clientHeight; // equals window dimension
        canvas.height = clientHeight;
        canvas.style.height = clientHeight + 'px';
        canvas.style.width = clientHeight + 'px';

        if (this.renderedImage && shouldReload) {
            let image = new Image();
            image.src = this.renderedImage;
            image.onload = () => {
                this.drawScaledImage(image, canvas.getContext('2d'));
            };
        } else if (shouldReload) {
            //reload it
            this.load();
        }

        return {
            width: clientHeight,
            height: clientHeight,
        };
    }

    componentDidMount() {
        let element = document.getElementById(this.getContainerId());

        if (!element) {
            warning('no element found: ' + this.getContainerId());
            return;
        }

        element.appendChild(this.createCanvasElement());

        //scale the canvas pre
        this.onWindowResize(false);

        //then do resize listeners
        const resize = () => {
            this.onWindowResize();
        };

        window.removeEventListener('resize', resize);
        window.addEventListener('resize', resize);

        //overwrite component will unmount
        this.componentWillUnmount = () => {
            window.removeEventListener('resize', resize);
            this.getCanvasElement().remove();
        };

        (async () => {
            await this.load();
            this.setState(
                {
                    loaded: true,
                },
                () => {
                    //finally render the canvas
                    this.getCanvasElement().style.display = 'block';
                }
            );
        })();
    }

    async load() {
        let canvas = this.getCanvasElement();
        let context = canvas.getContext('2d');
        let path = await this.props.token.getBase64EncodedPath('path');
        let image = await this.loadImage(path);
        this.drawScaledImage(image, context);

        let assets = this.props.token.getAssets() || [];

        for (let i = 0; i < assets.length; i++) {
            let asset = assets[i];

            if (!asset) continue;

            try {
                let assetPath = await this.props.token.getBase64EncodedAsset(
                    asset.assetId
                );
                let assetImage = await this.loadImage(assetPath);
                this.drawScaledImage(assetImage, context);
            } catch (error) {
                warning(error);
            }
        }

        return this.renderToken();
    }

    getDataURL() {
        if (!this.renderedImage) this.renderToken();
        return this.renderedImage;
    }

    renderToken() {
        let canvas = this.getCanvasElement();
        if (!canvas) return;

        let canvasCopy = document.createElement('canvas');
        canvasCopy.width = canvas.width;
        canvasCopy.height = canvas.height;
        canvasCopy.getContext('2d').drawImage(canvas, 0, 0); // copy the 'old' canva
        this.renderedImage = canvasCopy.toDataURL('image/png', 1.0);
        return this.renderedImage;
    }

    async loadImage(src: string) {
        let image = new Image();
        image.src = src;
        await new Promise((resolve) => {
            image.onload = resolve;
        });
        return image;
    }

    drawScaledImage(
        img: HTMLImageElement,
        context: CanvasRenderingContext2D,
        settings?: {
            x?: number;
            y?: number;
        }
    ) {
        let canvas = context.canvas;
        // get the scale
        // it is the min of the 2 ratios
        let scaleFactor = Math.max(
            canvas.width / img.width,
            canvas.height / img.height
        );

        // Finding the new width and height based on the scale factor
        let newWidth = img.width * scaleFactor;
        let newHeight = img.height * scaleFactor;

        // get the top left position of the image
        // in order to center the image within the canvas
        let x = settings?.x || canvas.width / 2 - newWidth / 2;
        let y = settings?.y || canvas.width / 2 - newHeight / 2;

        // When drawing the image, we have to scale down the image
        // width and height in order to fit within the canvas
        context.drawImage(img, x, y, newWidth, newHeight);
    }

    componentDidUpdate(
        prevProps: Readonly<RenderComponentProps>,
        prevState: Readonly<RenderComponentState>,
        snapshot?: any
    ): void {
        if (!this.state.loaded) return;

        let shouldQuit = prevProps.token.pathId === this.props.token.pathId;

        //check if assets have changed
        let oldAssets = prevProps.token.getAssets();
        let newAssets = this.props.token.getAssets();

        newAssets.forEach((asset, index) => {
            if (!asset && !oldAssets[index]) return;

            if (!asset && oldAssets[index]) shouldQuit = false;

            if (!oldAssets.find((val) => val.assetId === asset.assetId))
                shouldQuit = false;
        });

        if (shouldQuit) return;

        this.setState({
            loaded: false,
        });

        let canvas = this.getCanvasElement();
        canvas.style.display = 'none';

        let context = canvas.getContext('2d');
        context.clearRect(0, 0, canvas.height, canvas.width);
        //reload
        this.load().then(() => {
            canvas.style.display = 'block';
            this.setState({
                loaded: true,
            });
        });
    }

    getContainerId() {
        return (
            this.props.token.tokenId +
            '_' +
            this.props.token.project.name +
            '_' +
            this.props.token.project?.deployedProject?.network?.name +
            '_' +
            this.state.seed
        );
    }

    getCanvasID() {
        return this.getContainerId() + '_canvas';
    }

    getCanvasElement() {
        return document.getElementById(this.getCanvasID()) as HTMLCanvasElement;
    }

    createCanvasElement() {
        let canvas = document.createElement('canvas');
        canvas.style.display = 'none';
        canvas.style.marginLeft = 'auto';
        canvas.style.marginRight = 'auto';
        canvas.setAttribute('id', this.getContainerId() + '_canvas');
        return canvas;
    }

    render() {
        return (
            <div
                style={{
                    flex: '0 0 auto',
                }}
                id={this.props.token ? this.getContainerId() : ''}
                data-loading={!this.state.loaded}
            >
                {!this.state.loaded ? (
                    <div
                        style={{
                            textAlign: 'center',
                            padding: '12px',
                            border: '1px solid black',
                        }}
                    >
                        <h1>Rendering</h1>
                        <small>InfinityMint</small>
                        <p
                            style={{
                                fontSize: '2rem',
                            }}
                        >
                            {this.props.token.getPathName()}{' '}
                            <span
                                style={{
                                    fontSize: '1.25rem',
                                    border: '1px dashed red',
                                    color: 'red',
                                    padding: '4px',
                                }}
                            >
                                tokenId {this.props.token.tokenId}
                            </span>
                        </p>
                        <p
                            style={{
                                fontSize: '1.5rem',
                            }}
                        >
                            {this.props.token.getName() || 'Unknown Token'}
                        </p>
                    </div>
                ) : (
                    <></>
                )}
            </div>
        );
    }
}
