import {Actor, BaseAlign, Color, FontStyle, TextAlign, Vector} from "excalibur";
import * as excalibur from "excalibur";
import chain from "@softwareventures/chain";
import {mapFn, foldFn, concat, toArray} from "@softwareventures/iterable";
import {hasProperty} from "unknown";

export interface LabelOptions {
    /**  The text to draw. */
    readonly text?: string;

    /** The CSS font family string (e.g. `sans-serif`, `Droid Sans Pro`). Web
     * fonts are supported, same as in CSS. */
    readonly fontFamily?: string;

    /** The font style for the label.
     *
     * @default FontStyle.Normal */
    readonly fontStyle?: FontStyle;

    /** True if the text is bold.
     *
     * @default false */
    readonly bold?: boolean;

    /** The font size in pixels.
     *
     * @default 10 */
    readonly fontSize?: number;

    /** Horizontal text alignment.
     *
     * @default TextAlign.Left */
    readonly textAlign?: TextAlign;

    /** Baseline alignment.
     *
     * @default BaseAlign.Alphabetic */
    readonly baseAlign?: BaseAlign;

    /** The height of each line of text in pixels, for multiline text.
     *
     * Set to `undefined` to use the font size as the line height.
     *
     * @default undefined */
    readonly lineHeight?: number;

    /** The maximum width of a line of text, in pixels, after which the text
     * will wrap to the next line.
     *
     * Set to `Infinity` to disable text wrapping.
     *
     * @default Infinity */
    readonly wrapWidth?: number;

    /** The position of the text in pixels. */
    readonly pos?: Vector;

    /** The velocity of the text in pixels per second. */
    readonly vel?: Vector;

    /** The acceleration of the text in pixels per second per second. */
    readonly acc?: Vector;

    /** The rotation of the text in radians. */
    readonly rotation?: number;

    /** The rotational velocity of the text in radians per second. */
    readonly rx?: number;

    /** True if the text is visible, false if it is invisible.
     *
     * @default true */
    readonly visible?: boolean;

    /** The color of the text. */
    readonly color?: Color;

    /** The color of the text outline. Set to Color.Transparent to hide the outline.
     *
     * @default Color.Transparent */
    readonly outlineColor?: Color;

    /** The width of the text outline, in pixels. Set to 0 to hide the outline.
     *
     * @default 0 */
    readonly outlineWidth?: number;

    /** Overall opacity of the label, from 0 to 1.
     *
     * @default 1 */
    readonly alpha?: number;

    /** Do not use.
     *
     * @deprecated This field has an unwanted and confusing interaction with
     * the underlying Actor class.
     *
     * @see https://github.com/excaliburjs/Excalibur/issues/874#issuecomment-814557137 */
    readonly opacity?: number;

    /** The color of the shadow. Set to Color.Transparent to hide the shadow.
     *
     * @default Color.Transparent */
    readonly shadowColor?: Color;

    /** The offset of the shadow from the text, in pixels.
     *
     * @default Vector.Zero */
    readonly shadowOffset?: Vector;

    /** Radius of the shadow blur in pixels.
     *
     * @default 0 */
    readonly shadowBlurRadius?: number;
}

const offscreenCanvas = (() => {
    let cache: HTMLCanvasElement | null = null;

    return (onscreenCanvas: HTMLCanvasElement): HTMLCanvasElement | null => {
        if (cache == null) {
            cache = onscreenCanvas.ownerDocument?.createElement("canvas") ?? null;
        }

        if (cache != null) {
            cache.width = onscreenCanvas.width;
            cache.height = onscreenCanvas.height;
        }

        return cache;
    };
})();

export default class Label extends Actor {
    /**  The text to draw. */
    public text: string;

    /** True if the text is bold.
     *
     * @default false */
    public bold: boolean;

    /** The CSS font family string (e.g. `sans-serif`, `Droid Sans Pro`). Web fonts
     * are supported, same as in CSS. */
    public fontFamily: string;

    /** Font size in the selected units (`fontUnit`).
     *
     * @default 10 */
    public fontSize: number;

    /** The font style for this label
     *
     * @default FontStyle.Normal */
    public fontStyle: FontStyle;

    /** Horizontal text alignment.
     *
     * @default TextAlign.Left */
    public textAlign: TextAlign;

    /** Vertical baseline text alignment.
     *
     * @default BaseAlign.Bottom */
    public baseAlign: BaseAlign;

    /** The height of each line of text in pixels, for multiline text.
     *
     * Set to `undefined` to use the font size as the line height. */
    public lineHeight: number | undefined;

    /** The maximum width of a line of text, in pixels, after which the text
     * will wrap to the next line.
     *
     * Set to `Infinity` to disable text wrapping. */
    public wrapWidth: number;

    /** The color of the text outline. Set to Color.Transparent to hide the outline. */
    public outlineColor: Color;

    /** The width of the text outline, in pixels. Set to 0 to hide the outline. */
    public outlineWidth: number;

    /** The color of the shadow. Set to Color.Transparent to hide the shadow. */
    public shadowColor: Color;

    /** The offset of the shadow from the text, in pixels. */
    public shadowOffset: Vector;

    /** Radius of the shadow blur in pixels. */
    public shadowBlurRadius: number;

    /** Overall opacity of the label, from 0 to 1. */
    public alpha: number;

    /** Do not use.
     *
     * @deprecated This field has an unwanted and confusing interaction with
     * the underlying Actor class.
     *
     * @see https://github.com/excaliburjs/Excalibur/issues/874#issuecomment-814557137 */
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore Unused override to mark deprecated
    public opacity: number;

    public constructor(options?: LabelOptions) {
        super(options);
        this.text = options?.text ?? "";
        this.bold = options?.bold ?? false;
        this.fontFamily = options?.fontFamily ?? "sans-serif";
        this.fontSize = options?.fontSize ?? 10;
        this.fontStyle = options?.fontStyle ?? FontStyle.Normal;
        this.textAlign = options?.textAlign ?? TextAlign.Left;
        this.baseAlign = options?.baseAlign ?? BaseAlign.Bottom;
        this.lineHeight = options?.lineHeight;
        this.wrapWidth = options?.wrapWidth ?? Infinity;
        this.outlineColor = options?.outlineColor ?? Color.Transparent;
        this.outlineWidth = options?.outlineWidth ?? 0;
        this.shadowColor = options?.shadowColor ?? Color.Transparent;
        this.shadowOffset = options?.shadowOffset ?? Vector.Zero;
        this.shadowBlurRadius = options?.shadowBlurRadius ?? 0;
        this.alpha = options?.alpha ?? options?.opacity ?? 1;

        if (
            hasProperty(excalibur, "Flags") &&
            hasProperty(excalibur, "Legacy") &&
            hasProperty(excalibur.Legacy, "LegacyDrawing") &&
            !excalibur.Flags.isEnabled(excalibur.Legacy.LegacyDrawing)
        ) {
            throw new Error(
                "excalibur-extended-label requires you to call Flags.useLegacyDrawing() before constructing Excalibur Engine"
            );
        }
    }

    public override draw(context: CanvasRenderingContext2D, delta: number): void {
        const shadowVisible =
            this.shadowColor.a !== 0 &&
            (this.shadowBlurRadius !== 0 || this.shadowOffset.x !== 0 || this.shadowOffset.y !== 0);
        const canvas2 = shadowVisible ? offscreenCanvas(context.canvas) : null;
        const context2 = canvas2?.getContext("2d") ?? context;

        context2.save();

        if (context2 !== context) {
            context2.setTransform(context.getTransform());
        }

        context2.translate(this.pos.x, this.pos.y);
        context2.scale(this.scale.x, this.scale.y);
        context2.rotate(this.rotation);
        context2.textAlign = lookupTextAlign(this.textAlign);
        context2.textBaseline = lookupBaseAlign(this.baseAlign);
        context2.font = `${lookupFontStyle(this.fontStyle)} ${lookupFontWeight(this.bold)} ${
            this.fontSize
        }px ${this.fontFamily}`;
        context2.lineWidth = this.outlineWidth * 2;
        context2.strokeStyle =
            this.outlineWidth === 0 ? "transparent" : this.outlineColor.toString();
        context2.fillStyle = this.color.toString();

        const lines = this.wrapLines(context2);
        const lineHeight = this.lineHeight ?? this.fontSize;

        lines.forEach((line, i) => void context2.strokeText(line, 0, i * lineHeight));
        lines.forEach((line, i) => void context2.fillText(line, 0, i * lineHeight));

        context2.restore();

        if (canvas2 != null) {
            context.save();
            context.resetTransform();
            context.shadowBlur = this.shadowBlurRadius;
            context.shadowColor = this.shadowColor.toString();
            context.shadowOffsetX = this.shadowOffset.x;
            context.shadowOffsetY = this.shadowOffset.y;
            context.globalAlpha = this.alpha;
            context.drawImage(canvas2, 0, 0);
            context.restore();
        }
    }

    private wrapLines(context: CanvasRenderingContext2D): string[] {
        const lines = this.text.split("\n");
        if (isFinite(this.wrapWidth)) {
            return chain(lines)
                .map(mapFn(line => line.split(/\s+/u)))
                .map(
                    mapFn(
                        foldFn(
                            ([line, ...lines], word) =>
                                line == null
                                    ? [word]
                                    : context.measureText(`${line} ${word}`).width < this.wrapWidth
                                    ? [`${line} ${word}`, ...lines]
                                    : [word, line, ...lines],
                            [] as string[]
                        )
                    )
                )
                .map(concat)
                .map(toArray)
                .value.reverse();
        } else {
            return lines;
        }
    }
}

function lookupTextAlign(textAlign: TextAlign): CanvasTextAlign {
    switch (textAlign) {
        case TextAlign.Left:
            return "left";
        case TextAlign.Right:
            return "right";
        case TextAlign.Center:
            return "center";
        case TextAlign.End:
            return "end";
        case TextAlign.Start:
            return "start";
    }
}

function lookupBaseAlign(baseAlign: BaseAlign): CanvasTextBaseline {
    switch (baseAlign) {
        case BaseAlign.Alphabetic:
            return "alphabetic";
        case BaseAlign.Bottom:
            return "bottom";
        case BaseAlign.Hanging:
            return "hanging";
        case BaseAlign.Ideographic:
            return "ideographic";
        case BaseAlign.Middle:
            return "middle";
        case BaseAlign.Top:
            return "top";
    }
}

function lookupFontStyle(fontStyle: FontStyle): string {
    switch (fontStyle) {
        case FontStyle.Italic:
            return "italic";
        case FontStyle.Normal:
            return "normal";
        case FontStyle.Oblique:
            return "oblique";
    }
}

function lookupFontWeight(bold: boolean): string {
    return bold ? "bold" : "normal";
}
