// @flow

import Point from '@mapbox/point-geometry';

import {GLYPH_PBF_BORDER} from '../style/parse_glyph_pbf.js';

import type Anchor from './anchor.js';
import type {PositionedIcon, Shaping} from './shaping.js';
import {IMAGE_PADDING} from '../render/image_atlas.js';
import {SDF_SCALE} from '../render/glyph_manager.js';
import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer.js';
import type {Feature} from '../style-spec/expression/index.js';
import type {StyleImage} from '../style/style_image.js';
import {isVerticalClosePunctuation, isVerticalOpenPunctuation} from '../util/verticalize_punctuation.js';
import ONE_EM from './one_em.js';
import {warnOnce} from '../util/util.js';

export type TextureCoordinate = {
    x: number,
    y: number,
    w: number,
    h: number
};

type Size = {| fixed: number, stretch: number |};

/**
 * A textured quad for rendering a single icon or glyph.
 *
 * The zoom range the glyph can be shown is defined by minScale and maxScale.
 *
 * @param tl The offset of the top left corner from the anchor.
 * @param tr The offset of the top right corner from the anchor.
 * @param bl The offset of the bottom left corner from the anchor.
 * @param br The offset of the bottom right corner from the anchor.
 * @param texPrimary The texture coordinates of the primary image.
 * @param texSecondary The texture coordinates of an optional secondary image.
 *
 * @private
 */
export type SymbolQuad = {
    tl: Point,
    tr: Point,
    bl: Point,
    br: Point,
    texPrimary: TextureCoordinate,
    texSecondary: ?TextureCoordinate,
    pixelOffsetTL: Point,
    pixelOffsetBR: Point,
    writingMode: any | void,
    glyphOffset: [number, number],
    sectionIndex: number,
    isSDF: boolean,
    minFontScaleX: number,
    minFontScaleY: number
};

// If you have a 10px icon that isn't perfectly aligned to the pixel grid it will cover 11 actual
// pixels. The quad needs to be padded to account for this, otherwise they'll look slightly clipped
// on one edge in some cases.
const border = IMAGE_PADDING;

/**
 * Create the quads used for rendering an icon.
 * @private
 */
export function getIconQuads(
                      shapedIcon: PositionedIcon,
                      iconRotate: number,
                      isSDFIcon: boolean,
                      hasIconTextFit: boolean): Array<SymbolQuad> {
    const quads = [];

    const image = shapedIcon.imagePrimary;
    const pixelRatio = image.pixelRatio;
    const imageWidth = image.paddedRect.w - 2 * border;
    const imageHeight = image.paddedRect.h - 2 * border;

    const iconWidth = shapedIcon.right - shapedIcon.left;
    const iconHeight = shapedIcon.bottom - shapedIcon.top;

    const stretchX = image.stretchX || [[0, imageWidth]];
    const stretchY = image.stretchY || [[0, imageHeight]];

    const reduceRanges = (sum: number, range: [number, number]) => sum + range[1] - range[0];
    const stretchWidth = stretchX.reduce(reduceRanges, 0);
    const stretchHeight = stretchY.reduce(reduceRanges, 0);
    const fixedWidth = imageWidth - stretchWidth;
    const fixedHeight = imageHeight - stretchHeight;

    let stretchOffsetX = 0;
    let stretchContentWidth = stretchWidth;
    let stretchOffsetY = 0;
    let stretchContentHeight = stretchHeight;
    let fixedOffsetX = 0;
    let fixedContentWidth = fixedWidth;
    let fixedOffsetY = 0;
    let fixedContentHeight = fixedHeight;

    if (image.content && hasIconTextFit) {
        const content = image.content;
        stretchOffsetX = sumWithinRange(stretchX, 0, content[0]);
        stretchOffsetY = sumWithinRange(stretchY, 0, content[1]);
        stretchContentWidth = sumWithinRange(stretchX, content[0], content[2]);
        stretchContentHeight = sumWithinRange(stretchY, content[1], content[3]);
        fixedOffsetX = content[0] - stretchOffsetX;
        fixedOffsetY = content[1] - stretchOffsetY;
        fixedContentWidth = content[2] - content[0] - stretchContentWidth;
        fixedContentHeight = content[3] - content[1] - stretchContentHeight;
    }

    const makeBox = (left: Size, top: Size, right: Size, bottom: Size) => {

        const leftEm = getEmOffset(left.stretch - stretchOffsetX, stretchContentWidth, iconWidth, shapedIcon.left);
        const leftPx = getPxOffset(left.fixed - fixedOffsetX, fixedContentWidth, left.stretch, stretchWidth);

        const topEm = getEmOffset(top.stretch - stretchOffsetY, stretchContentHeight, iconHeight, shapedIcon.top);
        const topPx = getPxOffset(top.fixed - fixedOffsetY, fixedContentHeight, top.stretch, stretchHeight);

        const rightEm = getEmOffset(right.stretch - stretchOffsetX, stretchContentWidth, iconWidth, shapedIcon.left);
        const rightPx = getPxOffset(right.fixed - fixedOffsetX, fixedContentWidth, right.stretch, stretchWidth);

        const bottomEm = getEmOffset(bottom.stretch - stretchOffsetY, stretchContentHeight, iconHeight, shapedIcon.top);
        const bottomPx = getPxOffset(bottom.fixed - fixedOffsetY, fixedContentHeight, bottom.stretch, stretchHeight);

        const tl = new Point(leftEm, topEm);
        const tr = new Point(rightEm, topEm);
        const br = new Point(rightEm, bottomEm);
        const bl = new Point(leftEm, bottomEm);
        const pixelOffsetTL = new Point(leftPx / pixelRatio, topPx / pixelRatio);
        const pixelOffsetBR = new Point(rightPx / pixelRatio, bottomPx / pixelRatio);

        const angle = iconRotate * Math.PI / 180;

        if (angle) {
            const sin = Math.sin(angle),
                cos = Math.cos(angle),
                matrix = [cos, -sin, sin, cos];

            tl._matMult(matrix);
            tr._matMult(matrix);
            bl._matMult(matrix);
            br._matMult(matrix);
        }

        const x1 = left.stretch + left.fixed;
        const x2 = right.stretch + right.fixed;
        const y1 = top.stretch + top.fixed;
        const y2 = bottom.stretch + bottom.fixed;

        const subRect = {
            x: image.paddedRect.x + border + x1,
            y: image.paddedRect.y + border + y1,
            w: x2 - x1,
            h: y2 - y1
        };

        const imageSecondary = shapedIcon.imageSecondary;
        const subRectB = imageSecondary ? {
            x: imageSecondary.paddedRect.x + border + x1,
            y: imageSecondary.paddedRect.y + border + y1,
            w: x2 - x1,
            h: y2 - y1
        } : undefined;

        const minFontScaleX = fixedContentWidth / pixelRatio / iconWidth;
        const minFontScaleY = fixedContentHeight / pixelRatio / iconHeight;

        // Icon quad is padded, so texture coordinates also need to be padded.
        return {tl, tr, bl, br, texPrimary: subRect, texSecondary: subRectB, writingMode: undefined, glyphOffset: [0, 0], sectionIndex: 0, pixelOffsetTL, pixelOffsetBR, minFontScaleX, minFontScaleY, isSDF: isSDFIcon};
    };

    if (!hasIconTextFit || (!image.stretchX && !image.stretchY)) {
        quads.push(makeBox(
            {fixed: 0, stretch: -1},
            {fixed: 0, stretch: -1},
            {fixed: 0, stretch: imageWidth + 1},
            {fixed: 0, stretch: imageHeight + 1}));
    } else {
        const xCuts = stretchZonesToCuts(stretchX, fixedWidth, stretchWidth);
        const yCuts = stretchZonesToCuts(stretchY, fixedHeight, stretchHeight);

        for (let xi = 0; xi < xCuts.length - 1; xi++) {
            const x1 = xCuts[xi];
            const x2 = xCuts[xi + 1];
            for (let yi = 0; yi < yCuts.length - 1; yi++) {
                const y1 = yCuts[yi];
                const y2 = yCuts[yi + 1];
                quads.push(makeBox(x1, y1, x2, y2));
            }
        }
    }

    return quads;
}

function sumWithinRange(ranges: Array<[number, number]>, min: number, max: number) {
    let sum = 0;
    for (const range of ranges) {
        sum += Math.max(min, Math.min(max, range[1])) - Math.max(min, Math.min(max, range[0]));
    }
    return sum;
}

function stretchZonesToCuts(stretchZones: Array<[number, number]>, fixedSize: number, stretchSize: number) {
    const cuts = [{fixed: -border, stretch: 0}];

    for (const [c1, c2] of stretchZones) {
        const last = cuts[cuts.length - 1];
        cuts.push({
            fixed: c1 - last.stretch,
            stretch: last.stretch
        });
        cuts.push({
            fixed: c1 - last.stretch,
            stretch: last.stretch + (c2 - c1)
        });
    }
    cuts.push({
        fixed: fixedSize + border,
        stretch: stretchSize
    });
    return cuts;
}

function getEmOffset(stretchOffset: number, stretchSize: number, iconSize: number, iconOffset: number) {
    return stretchOffset / stretchSize * iconSize + iconOffset;
}

function getPxOffset(fixedOffset: number, fixedSize: number, stretchOffset: number, stretchSize: number) {
    return fixedOffset - fixedSize * stretchOffset / stretchSize;
}

function getRotateOffset(textOffset: [number, number]) {
    const x = textOffset[0], y = textOffset[1];
    const product = x * y;
    if (product > 0) {
        return [x, -y];
    } else if (product < 0) {
        return [-x, y];
    } else if (x === 0) {
        return [y, x];
    } else {
        return [y, -x];
    }
}

function getMidlineOffset(shaping: Shaping, lineHeight: number, previousOffset: number, lineIndex: number) {
    const currentLineHeight = (lineHeight + shaping.positionedLines[lineIndex].lineOffset);
    if (lineIndex === 0) {
        return previousOffset + currentLineHeight / 2.0;
    }
    const aboveLineHeight = (lineHeight + shaping.positionedLines[lineIndex - 1].lineOffset);
    return previousOffset + (currentLineHeight + aboveLineHeight) / 2.0;
}

/**
 * Create the quads used for rendering a text label.
 * @private
 */
export function getGlyphQuads(anchor: Anchor,
                       shaping: Shaping,
                       textOffset: [number, number],
                       layer: SymbolStyleLayer,
                       alongLine: boolean,
                       feature: Feature,
                       imageMap: {[_: string]: StyleImage},
                       allowVerticalPlacement: boolean): Array<SymbolQuad> {
    const quads = [];
    if (shaping.positionedLines.length === 0) return quads;

    const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}) * Math.PI / 180;
    const rotateOffset = getRotateOffset(textOffset);

    let shapingHeight = Math.abs(shaping.top - shaping.bottom);
    for (const line of shaping.positionedLines) {
        shapingHeight -= line.lineOffset;
    }
    const lineCounts = shaping.positionedLines.length;
    const lineHeight = shapingHeight / lineCounts;
    let currentOffset = shaping.top - textOffset[1];
    for (let lineIndex = 0; lineIndex < lineCounts; ++lineIndex) {
        const line = shaping.positionedLines[lineIndex];
        currentOffset = getMidlineOffset(shaping, lineHeight, currentOffset, lineIndex);
        for (const positionedGlyph of line.positionedGlyphs) {
            if (!positionedGlyph.rect) continue;
            const textureRect = positionedGlyph.rect || {};

            // The rects have an additional buffer that is not included in their size.
            const glyphPadding = 1.0;
            let rectBuffer = GLYPH_PBF_BORDER + glyphPadding;
            let isSDF = true;
            let pixelRatio = 1.0;
            let lineOffset = 0.0;
            if (positionedGlyph.imageName) {
                const image = imageMap[positionedGlyph.imageName];
                if (!image) continue;
                if (image.sdf) {
                    warnOnce("SDF images are not supported in formatted text and will be ignored.");
                    continue;
                }
                isSDF = false;
                pixelRatio = image.pixelRatio;
                rectBuffer = IMAGE_PADDING / pixelRatio;
            }

            const rotateVerticalGlyph = (alongLine || allowVerticalPlacement) && positionedGlyph.vertical;
            const halfAdvance = positionedGlyph.metrics.advance * positionedGlyph.scale / 2;
            const metrics = positionedGlyph.metrics;
            const rect = positionedGlyph.rect;
            if (rect === null) continue;

            // Align images and scaled glyphs in the middle of a vertical line.
            if (allowVerticalPlacement && shaping.verticalizable) {
                // image's advance for vertical shaping is its height, so that we have to take the difference into
                // account after image glyph is rotated
                lineOffset = positionedGlyph.imageName ? halfAdvance - positionedGlyph.metrics.width * positionedGlyph.scale / 2.0 : 0;
            }

            const glyphOffset = alongLine ?
                [positionedGlyph.x + halfAdvance, positionedGlyph.y] :
                [0, 0];

            let builtInOffset = [0, 0];
            let verticalizedLabelOffset = [0, 0];
            let useRotateOffset = false;
            if (!alongLine) {
                if (rotateVerticalGlyph) {
                // Vertical POI labels that are rotated 90deg CW and whose glyphs must preserve upright orientation
                // need to be rotated 90deg CCW. After a quad is rotated, it is translated to the original built-in offset.
                    verticalizedLabelOffset =
                        [positionedGlyph.x + halfAdvance + rotateOffset[0], positionedGlyph.y + rotateOffset[1] - lineOffset];
                    useRotateOffset = true;
                } else {
                    builtInOffset =  [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1] - lineOffset];
                }
            }

            const paddedWidth =
                rect.w * positionedGlyph.scale / (pixelRatio * (positionedGlyph.localGlyph ? SDF_SCALE : 1));
            const  paddedHeight =
                rect.h * positionedGlyph.scale / (pixelRatio * (positionedGlyph.localGlyph ? SDF_SCALE : 1));

            let tl, tr, bl, br;
            if (!rotateVerticalGlyph) {
                const x1 = (metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0];
                const y1 = (-metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1];
                const x2 = x1 + paddedWidth;
                const y2 = y1 + paddedHeight;

                tl = new Point(x1, y1);
                tr = new Point(x2, y1);
                bl = new Point(x1, y2);
                br = new Point(x2, y2);
            } else {
                // For vertical glyph placement, follow the steps to put the glyph bitmap in right coordinates:
                // 1. Rotate the glyph by using original glyph coordinates instead of padded coordinates, since the
                // rotation center and xOffsetCorrection are all based on original glyph's size.
                // 2. Do x offset correction so that 'tl' is shifted to the same x coordinate before rotation.
                // 3. Adjust glyph positon for 'tl' by applying vertial padding and horizontal shift, now 'tl' is the
                // coordinate where we draw the glyph bitmap.
                // 4. Calculate other three bitmap coordinates.

                // Vertical-supporting glyphs are laid out in 24x24 point boxes (1 square em)
                // In horizontal orientation, the "yShift" is the negative value of the height that
                // the glyph is above the horizontal midline.
                // By rotating counter-clockwise around the point at the center of the left
                // edge of a 24x24 layout box centered below the midline, we align the midline
                // of the rotated glyphs with the horizontal midline, so the yShift is no longer
                // necessary, but we also pull the glyph to the left along the x axis.
                const yShift = (positionedGlyph.y - currentOffset);
                const center = new Point(-halfAdvance, halfAdvance - yShift);
                const verticalRotation = -Math.PI / 2;
                const verticalOffsetCorrection = new Point(...verticalizedLabelOffset);
                // Relative position before rotation
                // tl ----- tr
                //   |     |
                //   |     |
                // bl ----- br
                tl = new Point(-halfAdvance + builtInOffset[0], builtInOffset[1]);
                tl._rotateAround(verticalRotation, center)._add(verticalOffsetCorrection);

                // Relative position after rotating
                // tr ----- br
                //   |     |
                //   |     |
                // tl ----- bl
                // After rotation, glyph lies on the horizontal midline.
                // Shift back to tl's original x coordinate before rotation by applying 'xOffsetCorrection'.
                tl.x += -yShift + halfAdvance;

                // Add padding for y coordinate's justification
                tl.y -= (metrics.left - rectBuffer) * positionedGlyph.scale;

                // Adjust x coordinate according to glyph bitmap's height and the vectical advance
                const verticalAdvance = positionedGlyph.imageName ? metrics.advance * positionedGlyph.scale :
                    ONE_EM * positionedGlyph.scale;
                // Check wether the glyph is generated from server side or locally
                const chr = String.fromCodePoint(positionedGlyph.glyph);
                if (isVerticalClosePunctuation(chr)) {
                    // Place vertical punctuation in right place, pull down 1 pixel's space for close punctuations
                    tl.x += (-rectBuffer + 1) * positionedGlyph.scale;
                } else if (isVerticalOpenPunctuation(chr)) {
                    const xOffset = verticalAdvance - metrics.height * positionedGlyph.scale;
                    // Place vertical punctuation in right place, pull up 1 pixel's space for open punctuations
                    tl.x += xOffset + (-rectBuffer - 1) * positionedGlyph.scale;
                } else if (!positionedGlyph.imageName &&
                           ((metrics.width + rectBuffer * 2) !== rect.w || metrics.height + rectBuffer * 2 !== rect.h)) {
                    // Locally generated glyphs' bitmap do not have exact 'rectBuffer' padded around the glyphs,
                    // but the original tl do have distance of rectBuffer padded to the top of the glyph.
                    const perfectPaddedHeight = (metrics.height + rectBuffer * 2) * positionedGlyph.scale;
                    const delta = verticalAdvance - perfectPaddedHeight;
                    tl.x += delta / 2;
                } else {
                    // Place the glyph bitmap right in the center of the 24x24 point boxes
                    const delta = verticalAdvance - paddedHeight;
                    tl.x += delta / 2;
                }
                // Calculate other three points
                tr = new Point(tl.x, tl.y - paddedWidth);
                bl = new Point(tl.x + paddedHeight, tl.y);
                br = new Point(tl.x + paddedHeight, tl.y - paddedWidth);
            }

            if (textRotate) {
                let center;
                if (!alongLine) {
                    if (useRotateOffset) {
                        center = new Point(rotateOffset[0], rotateOffset[1]);
                    } else {
                        center = new Point(textOffset[0], textOffset[1]);
                    }
                } else {
                    center = new Point(0, 0);
                }
                tl._rotateAround(textRotate, center);
                tr._rotateAround(textRotate, center);
                bl._rotateAround(textRotate, center);
                br._rotateAround(textRotate, center);
            }

            const pixelOffsetTL = new Point(0, 0);
            const pixelOffsetBR = new Point(0, 0);
            const minFontScaleX = 0;
            const minFontScaleY = 0;
            quads.push({tl, tr, bl, br, texPrimary: textureRect, texSecondary: undefined, writingMode: shaping.writingMode, glyphOffset, sectionIndex: positionedGlyph.sectionIndex, isSDF, pixelOffsetTL, pixelOffsetBR, minFontScaleX, minFontScaleY});
        }
    }

    return quads;
}
