/* typehints:start */
import { AtlasSprite } from "./sprites";
import { DrawParameters } from "./draw_parameters";
/* typehints:end */

import { Vector } from "./vector";
import { Rectangle } from "./rectangle";
import { createLogger } from "./logging";

const logger = createLogger("draw_utils");

export function initDrawUtils() {
    CanvasRenderingContext2D.prototype.beginRoundedRect = function (x, y, w, h, r) {
        if (r < 0.05) {
            this.beginPath();
            this.rect(x, y, w, h);
            return;
        }

        if (w < 2 * r) {
            r = w / 2;
        }
        if (h < 2 * r) {
            r = h / 2;
        }
        this.beginPath();
        this.moveTo(x + r, y);
        this.arcTo(x + w, y, x + w, y + h, r);
        this.arcTo(x + w, y + h, x, y + h, r);
        this.arcTo(x, y + h, x, y, r);
        this.arcTo(x, y, x + w, y, r);
        // this.closePath();
    };

    CanvasRenderingContext2D.prototype.beginCircle = function (x, y, r) {
        if (r < 0.05) {
            this.beginPath();
            this.rect(x, y, 1, 1);
            return;
        }
        this.beginPath();
        this.arc(x, y, r, 0, 2.0 * Math.PI);
    };
}

/**
 *
 * @param {object} param0
 * @param {DrawParameters} param0.parameters
 * @param {AtlasSprite} param0.sprite
 * @param {number} param0.x
 * @param {number} param0.y
 * @param {number} param0.angle
 * @param {number} param0.size
 * @param {number=} param0.offsetX
 * @param {number=} param0.offsetY
 */
export function drawRotatedSprite({ parameters, sprite, x, y, angle, size, offsetX = 0, offsetY = 0 }) {
    parameters.context.translate(x, y);
    parameters.context.rotate(angle);
    sprite.drawCachedCentered(parameters, offsetX, offsetY, size, false);
    parameters.context.rotate(-angle);
    parameters.context.translate(-x, -y);
}

export function drawLineFast(context, { x1, x2, y1, y2, color = null, lineSize = 1 }) {
    const dX = x2 - x1;
    const dY = y2 - y1;

    const angle = Math.atan2(dY, dX) + 0.0 * Math.PI;
    const len = Math.hypot(dX, dY);

    context.translate(x1, y1);
    context.rotate(angle);

    if (color) {
        context.fillStyle = color;
    }

    context.fillRect(0, -lineSize / 2, len, lineSize);

    context.rotate(-angle);
    context.translate(-x1, -y1);
}

const INSIDE = 0;
const LEFT = 1;
const RIGHT = 2;
const BOTTOM = 4;
const TOP = 8;

// https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm

function computeOutCode(x, y, xmin, xmax, ymin, ymax) {
    let code = INSIDE;

    if (x < xmin)
        // to the left of clip window
        code |= LEFT;
    else if (x > xmax)
        // to the right of clip window
        code |= RIGHT;
    if (y < ymin)
        // below the clip window
        code |= BOTTOM;
    else if (y > ymax)
        // above the clip window
        code |= TOP;

    return code;
}

// Cohen–Sutherland clipping algorithm clips a line from
// P0 = (x0, y0) to P1 = (x1, y1) against a rectangle with
// diagonal from (xmin, ymin) to (xmax, ymax).
/**
 *
 * @param {CanvasRenderingContext2D} context
 */
export function drawLineFastClipped(context, rect, { x0, y0, x1, y1, color = null, lineSize = 1 }) {
    const xmin = rect.x;
    const ymin = rect.y;
    const xmax = rect.right();
    const ymax = rect.bottom();

    // compute outcodes for P0, P1, and whatever point lies outside the clip rectangle
    let outcode0 = computeOutCode(x0, y0, xmin, xmax, ymin, ymax);
    let outcode1 = computeOutCode(x1, y1, xmin, xmax, ymin, ymax);
    let accept = false;

    // eslint-disable-next-line no-constant-condition
    while (true) {
        if (!(outcode0 | outcode1)) {
            // bitwise OR is 0: both points inside window; trivially accept and exit loop
            accept = true;
            break;
        } else if (outcode0 & outcode1) {
            // bitwise AND is not 0: both points share an outside zone (LEFT, RIGHT, TOP,
            // or BOTTOM), so both must be outside window; exit loop (accept is false)
            break;
        } else {
            // failed both tests, so calculate the line segment to clip
            // from an outside point to an intersection with clip edge
            let x, y;

            // At least one endpoint is outside the clip rectangle; pick it.
            let outcodeOut = outcode0 ? outcode0 : outcode1;

            // Now find the intersection point;
            // use formulas:
            //   slope = (y1 - y0) / (x1 - x0)
            //   x = x0 + (1 / slope) * (ym - y0), where ym is ymin or ymax
            //   y = y0 + slope * (xm - x0), where xm is xmin or xmax
            // No need to worry about divide-by-zero because, in each case, the
            // outcode bit being tested guarantees the denominator is non-zero
            if (outcodeOut & TOP) {
                // point is above the clip window
                x = x0 + ((x1 - x0) * (ymax - y0)) / (y1 - y0);
                y = ymax;
            } else if (outcodeOut & BOTTOM) {
                // point is below the clip window
                x = x0 + ((x1 - x0) * (ymin - y0)) / (y1 - y0);
                y = ymin;
            } else if (outcodeOut & RIGHT) {
                // point is to the right of clip window
                y = y0 + ((y1 - y0) * (xmax - x0)) / (x1 - x0);
                x = xmax;
            } else if (outcodeOut & LEFT) {
                // point is to the left of clip window
                y = y0 + ((y1 - y0) * (xmin - x0)) / (x1 - x0);
                x = xmin;
            }

            // Now we move outside point to intersection point to clip
            // and get ready for next pass.
            if (outcodeOut == outcode0) {
                x0 = x;
                y0 = y;
                outcode0 = computeOutCode(x0, y0, xmin, xmax, ymin, ymax);
            } else {
                x1 = x;
                y1 = y;
                outcode1 = computeOutCode(x1, y1, xmin, xmax, ymin, ymax);
            }
        }
    }
    if (accept) {
        // Following functions are left for implementation by user based on
        // their platform (OpenGL/graphics.h etc.)
        // DrawRectangle(xmin, ymin, xmax, ymax);
        // LineSegment(x0, y0, x1, y1);
        drawLineFast(context, {
            x1: x0,
            y1: y0,
            x2: x1,
            y2: y1,
            color,
            lineSize,
        });
    }
}

/**
 * Converts an HSL color value to RGB. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
 * Assumes h, s, and l are contained in the set [0, 1] and
 * returns r, g, and b in the set [0, 255].
 *
 * @param   {number}  h       The hue
 * @param   {number}  s       The saturation
 * @param   {number}  l       The lightness
 * @return  {Array}           The RGB representation
 */
export function hslToRgb(h, s, l) {
    let r;
    let g;
    let b;

    if (s === 0) {
        r = g = b = l; // achromatic
    } else {
        // tslint:disable-next-line:no-shadowed-variable
        const hue2rgb = function (p, q, t) {
            if (t < 0) {
                t += 1;
            }
            if (t > 1) {
                t -= 1;
            }
            if (t < 1 / 6) {
                return p + (q - p) * 6 * t;
            }
            if (t < 1 / 2) {
                return q;
            }
            if (t < 2 / 3) {
                return p + (q - p) * (2 / 3 - t) * 6;
            }
            return p;
        };

        let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        let p = 2 * l - q;
        r = hue2rgb(p, q, h + 1 / 3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1 / 3);
    }

    return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}

export function wrapText(context, text, x, y, maxWidth, lineHeight, stroke = false) {
    var words = text.split(" ");
    var line = "";

    for (var n = 0; n < words.length; n++) {
        var testLine = line + words[n] + " ";
        var metrics = context.measureText(testLine);
        var testWidth = metrics.width;
        if (testWidth > maxWidth && n > 0) {
            if (stroke) {
                context.strokeText(line, x, y);
            } else {
                context.fillText(line, x, y);
            }
            line = words[n] + " ";
            y += lineHeight;
        } else {
            line = testLine;
        }
    }

    if (stroke) {
        context.strokeText(line, x, y);
    } else {
        context.fillText(line, x, y);
    }
}

/**
 * Returns a rotated trapez, used for spotlight culling
 * @param {number} x
 * @param {number} y
 * @param {number} w
 * @param {number} h
 * @param {number} leftHeight
 * @param {number} angle
 */
export function rotateTrapezRightFaced(x, y, w, h, leftHeight, angle) {
    const halfY = y + h / 2;
    const points = [
        new Vector(x, halfY - leftHeight / 2),
        new Vector(x + w, y),
        new Vector(x, halfY + leftHeight / 2),
        new Vector(x + w, y + h),
    ];

    return Rectangle.getAroundPointsRotated(points, angle);
}

/**
 * Converts values from 0 .. 255 to values like 07, 7f, 5d etc
 * @param {number} value
 * @returns {string}
 */
export function mapClampedColorValueToHex(value) {
    const hex = "0123456789abcdef";
    return hex[Math.floor(value / 16)] + hex[value % 16];
}

/**
 * Converts rgb to a hex string
 * @param {number} r
 * @param {number} g
 * @param {number} b
 * @returns {string}
 */
export function rgbToHex(r, g, b) {
    return mapClampedColorValueToHex(r) + mapClampedColorValueToHex(g) + mapClampedColorValueToHex(b);
}
