// Copyright (c) 2017, Shellyl_N and Authors
// license: ISC
// https://github.com/shellyln



import * as RedAgate          from 'red-agate/modules/red-agate';
import { SvgCanvas,
         SvgTextAttributes,
         TextAlignValue,
         TextBaselineValue }  from 'red-agate-svg-canvas/modules/drawing/canvas/SvgCanvas';
import { Rect2D }             from 'red-agate-svg-canvas/modules/drawing/canvas/TransferMatrix2D';
import { WebColor }           from 'red-agate-svg-canvas/modules/drawing/canvas/WebColor';
import { ShapeProps,
         shapePropsDefault,
         Shape,
         ImagingShapeBasePropsMixin,
         renderSvgCanvas,
         toImgTag,
         toElementStyle,
         toDataUrl,
         toSvg,
         CONTEXT_SVG_CANVAS } from 'red-agate/modules/red-agate/tags/Shape';
import { Gf2e8Field }         from 'red-agate-math/modules/math/Gf2Ext';
import { BCH }                from 'red-agate-math/modules/error-correction/BCH';
import { ReedSolomon }        from 'red-agate-math/modules/error-correction/ReedSolomon';
import { BitStreamWriter }    from 'red-agate-util/modules/io/BitStream';
import { TextEncoding }       from 'red-agate-util/modules/convert/TextEncoding';
import { Bitmap }             from 'red-agate-util/modules/imaging/Bitmap';
import { QrSourceDataTypes,
         QrDataChunkType,
         ecLevelMap,
         numberModeCharMap,
         alnumModeCharMap }   from "./Qr.defs";
import * as qr                from './data/Qr.m2.data';



export interface QrProps extends ShapeProps, ImagingShapeBasePropsMixin {
    data?: Array<Uint8Array | string | number> | Uint8Array | string;
    version?: number | "auto";
    ecLevel?: "L" | "M" | "Q" | "H";
    encoding?: "number" | "alnum" | "8bit" | "auto";
    cellSize?: number;
    unit?: string;
    asDataUrl?: boolean;
    asImgTag?: boolean;
}

export interface QrPropsNoUndefined extends ShapeProps, ImagingShapeBasePropsMixin {
    data?: Array<Uint8Array | string | number> | Uint8Array | string;
    version: number | "auto";
    ecLevel: "L" | "M" | "Q" | "H";
    encoding: "number" | "alnum" | "8bit" | "auto";
    cellSize: number;
    unit?: string;
    asDataUrl?: boolean;
    asImgTag?: boolean;
}

export const qrPropsDefault: QrPropsNoUndefined = Object.assign({}, shapePropsDefault, {
    version: "auto",
    ecLevel: "M",
    encoding: "auto",
    cellSize: 0.33,
    unit: "mm",
} as any);



const field = new Gf2e8Field();
let gxList: number[][] = [];

const masks = [
    { index: 0x00, fn: (x: number, y: number) =>   ((x + y) % 2) === 0 },
    { index: 0x01, fn: (x: number, y: number) =>   (     y  % 2) === 0 },
    { index: 0x02, fn: (x: number, y: number) =>   ( x      % 3) === 0 },
    { index: 0x03, fn: (x: number, y: number) =>   ((x + y) % 3) === 0 },
    { index: 0x04, fn: (x: number, y: number) => ((Math.floor(x / 3) + Math.floor(y / 2)) % 2) === 0},
    { index: 0x05, fn: (x: number, y: number) => ( ((x * y) % 2) + (x * y) % 3)      === 0},
    { index: 0x06, fn: (x: number, y: number) => ((((x * y) % 2) + (x * y) % 3) % 2) === 0},
    { index: 0x07, fn: (x: number, y: number) => ((((x * y) % 3) + (x + y) % 2) % 2) === 0}
];



export class Qr extends Shape<QrProps> {
    public constructor(props: QrProps) {
        super(Object.assign({}, qrPropsDefault, props));
    }

    public toImgTag(): string {
        return toImgTag(this);
    }

    public toElementStyle(): string {
        return toElementStyle(this);
    }

    public toDataUrl(): string {
        return toDataUrl(this);
    }

    public toSvg(): string {
        return toSvg(this);
    }

    public toRendered(): string {
        return RedAgate.renderAsHtml_noDefer(this);
    }

    public render(contexts: Map<string, any>, children: string) {
        let canvas: SvgCanvas = this.getContext(contexts, CONTEXT_SVG_CANVAS);
        const contextHasCanvas = Boolean(canvas);
        if (!contextHasCanvas) {
            canvas = new SvgCanvas();
            this.setContext(contexts, CONTEXT_SVG_CANVAS, canvas);
            super.beforeRender(contexts);
        }

        const data = this.props.data || "";

        const encoded = this.encodeData(data);
        const bitmap  = this.buildBitmap(encoded.data, encoded.version);
        this.drawBitmap(canvas, bitmap);

        if (contextHasCanvas) {
            return ``;
        } else {
            super.afterRender(contexts);
            this.unsetContext(contexts, CONTEXT_SVG_CANVAS);
            const total = bitmap.width * (this.props.cellSize as number) + (this.props.margin as number) * 2;
            const imageWidth  = total + (this.props.x || 0);
            const imageHeight = total + (this.props.y || 0);
            return renderSvgCanvas(this.props, canvas, imageWidth, imageHeight);
        }
    }


    protected evaluteMask(bitmap: Bitmap): number {
        const nx = bitmap.width,
              ny = bitmap.height;
        let   z  = 0;
        for (let i = 0; i < ny; i++) {
            let p: number | null = null;
            let c = 1;
            for (let j = 0; j < nx; j++) {
                const q = bitmap.get(j, i);
                if      (p !== q) c  = 1;
                else              c += 1;
                if      (5 === c) z += 3;
                else if (5   < c) z += 1;
                p = q;
            }
        }
        for (let i = 0; i < nx; i++) {
            let p: number | null = null;
            let c = 1;
            for (let j = 0; j < ny; j++) {
                const q = bitmap.get(i, j);
                if      (p !== q) c  = 1;
                else              c += 1;
                if      (5 === c) z += 3;
                else if (5   < c) z += 1;
                p = q;
            }
        }
        for (let i = 0; i < (ny - 1); i++) {
            for (let j = 0; j < (nx - 1); j++) {
                const v = bitmap.get(j, i);
                if (v === bitmap.get(j + 1, i    ) &&
                    v === bitmap.get(j    , i + 1) &&
                    v === bitmap.get(j + 1, i + 1)) {
                    z += 3;
                }
            }
        }
        for (let i = 0; i < ny; i++) {
            for (let j = 0; j < (nx - 10); j++) {
                if (bitmap.get(j     , i) !== 0 &&
                    bitmap.get(j +  1, i) === 0 &&
                    bitmap.get(j +  2, i) !== 0 &&
                    bitmap.get(j +  3, i) !== 0 &&
                    bitmap.get(j +  4, i) !== 0 &&
                    bitmap.get(j +  5, i) === 0 &&
                    bitmap.get(j +  6, i) !== 0 &&
                    bitmap.get(j +  7, i) === 0 &&
                    bitmap.get(j +  8, i) === 0 &&
                    bitmap.get(j +  9, i) === 0 &&
                    bitmap.get(j + 10, i) === 0) {
                    z += 40;
                } else if (
                    bitmap.get(j     , i) === 0 &&
                    bitmap.get(j +  1, i) === 0 &&
                    bitmap.get(j +  2, i) === 0 &&
                    bitmap.get(j +  3, i) === 0 &&
                    bitmap.get(j +  4, i) !== 0 &&
                    bitmap.get(j +  5, i) === 0 &&
                    bitmap.get(j +  6, i) !== 0 &&
                    bitmap.get(j +  7, i) !== 0 &&
                    bitmap.get(j +  8, i) !== 0 &&
                    bitmap.get(j +  9, i) === 0 &&
                    bitmap.get(j + 10, i) !== 0) {
                    z += 40;
                }
            }
        }
        for (let i = 0; i < nx; i++) {
            for (let j = 0; j < (ny - 10); j++) {
                if (bitmap.get(i, j     ) !== 0 &&
                    bitmap.get(i, j +  1) === 0 &&
                    bitmap.get(i, j +  2) !== 0 &&
                    bitmap.get(i, j +  3) !== 0 &&
                    bitmap.get(i, j +  4) !== 0 &&
                    bitmap.get(i, j +  5) === 0 &&
                    bitmap.get(i, j +  6) !== 0 &&
                    bitmap.get(i, j +  7) === 0 &&
                    bitmap.get(i, j +  8) === 0 &&
                    bitmap.get(i, j +  9) === 0 &&
                    bitmap.get(i, j + 10) === 0) {
                    z += 40;
                } else if (
                    bitmap.get(i, j     ) === 0 &&
                    bitmap.get(i, j +  1) === 0 &&
                    bitmap.get(i, j +  2) === 0 &&
                    bitmap.get(i, j +  3) === 0 &&
                    bitmap.get(i, j +  4) !== 0 &&
                    bitmap.get(i, j +  5) === 0 &&
                    bitmap.get(i, j +  6) !== 0 &&
                    bitmap.get(i, j +  7) !== 0 &&
                    bitmap.get(i, j +  8) !== 0 &&
                    bitmap.get(i, j +  9) === 0 &&
                    bitmap.get(i, j + 10) !== 0) {
                    z += 40;
                }
            }
        }
        let white = 0, black = 0;
        for (let i = 0; i < ny; i++) {
            for (let j = 0; j < nx; j++) {
                if (bitmap.get(j, i) === 0) { white++; }
                else                        { black++; }
            }
        }
        z += Math.floor(Math.abs((black / (white + black)) * 100 - 50) / 5) * 10;
        return z;
    }


    protected encodeNumberData(data: string): {data: BitStreamWriter, charLength: number} | null {
        const length = Math.ceil(data.length * 10 / 24);
        const buf = new BitStreamWriter(length);

        let i = 0, v = 0;
        for (; i < data.length; i++) {
            const c = numberModeCharMap.get(data[i]);
            if (c === void 0) return null;
            v = v * 10 + c;
            if (2 === (i % 3)) {
                // 10 bits
                buf.writeBits(v, 10);
                v = 0;
            }
        }
        i = i % 3;
        if (i) {
            // i is  1: 4bits, 2: 7bits
            buf.writeBits8(v, 1 + 3 * i);
        }
        return {data: buf, charLength: data.length};
    }

    protected encodeAlnumData(data: string): {data: BitStreamWriter, charLength: number} | null {
        const length = Math.ceil(data.length * 11 / 16);
        const buf = new BitStreamWriter(length);

        let i = 0, v = 0;
        for (; i < data.length; i++) {
            const c = alnumModeCharMap.get(data[i]);
            if (c === void 0) return null;
            v = v * 45 + c;
            if (1 === (i % 2)) {
                // 11 bits
                buf.writeBits(v, 11);
                v = 0;
            }
        }
        if (i % 2) {
            buf.writeBits8(v, 6);
        }
        return {data: buf, charLength: data.length};
    }

    protected encode8bitData(data: string): {data: BitStreamWriter, charLength: number} {
        const buf = new BitStreamWriter(0, TextEncoding.encodeToUtf8(data));
        return {data: buf, charLength: buf.byteLength};
    }

    protected encodeChunks(data: QrSourceDataTypes[] | QrSourceDataTypes): Array<{
                                  type: QrDataChunkType, data: BitStreamWriter, charLength: number }> {

        if (!Array.isArray(data)) {
            data = [data];
        }

        const chunks: Array<{type: QrDataChunkType, data: BitStreamWriter, charLength: number}> = [];

        for (const d of data) {
            if (d instanceof BitStreamWriter) chunks.push({type: QrDataChunkType.Binary, data: d, charLength: d.byteLength});
            else if (d instanceof Uint8Array) chunks.push({type: QrDataChunkType.Binary, data: new BitStreamWriter(0, d), charLength: d.length});
            else if (typeof d === "string") {
                let chunk: {data: BitStreamWriter, charLength: number} | null = null;
                let type: QrDataChunkType;
                let isManual = true;
                switch (this.props.encoding) {
                case "auto":
                    isManual = false;
                case "number":
                    chunk = this.encodeNumberData(d);
                    type = QrDataChunkType.Number;
                    if (isManual || chunk) break;
                case "alnum":
                    chunk = this.encodeAlnumData(d);
                    type = QrDataChunkType.Alnum;
                    if (isManual || chunk) break;
                case "8bit": default:
                    chunk = this.encode8bitData(d);
                    type = QrDataChunkType.Binary;
                    break;
                }
                if (chunk === null) {
                    throw new Error("QrModel2: character is out of range.");
                }
                chunks.push({type, data: chunk.data, charLength: chunk.charLength});
            }
            else if (typeof d === "number") {
                // TODO: not imple
            }
        }

        return chunks;
    }

    protected determineSymbolVersion(chunks: Array<{type: QrDataChunkType, data: BitStreamWriter}>):
        { version: number, segments: number[][], dataLength: number, maxDataLength: number } {

        let version = this.props.version === "auto" ? 1 : (this.props.version || 1);
        let segments: number[][];
        let maxDataLength: number = 0;
        let dataLength: number = 0;

        while (true) {
            dataLength = 0;

            for (const c of chunks) {
                switch (c.type) {
                case QrDataChunkType.Number:
                    if      (version < 10) dataLength += 14;
                    else if (version < 27) dataLength += 16;
                    else                   dataLength += 18;
                    break;
                case QrDataChunkType.Alnum:
                    if      (version < 10) dataLength += 13;
                    else if (version < 27) dataLength += 15;
                    else                   dataLength += 17;
                    break;
                case QrDataChunkType.Binary:
                    if      (version < 10) dataLength += 12;
                    else                   dataLength += 20;
                    break;
                }
                dataLength += c.data.bitLength;
            }

            switch (this.props.ecLevel) {
            case "L":
                segments = qr.segments[version].L;
                maxDataLength = 8 * qr.dataCodewords.L[version];
                break;
            case "Q":
                segments = qr.segments[version].Q;
                maxDataLength = 8 * qr.dataCodewords.Q[version];
                break;
            case "H":
                segments = qr.segments[version].H;
                maxDataLength = 8 * qr.dataCodewords.H[version];
                break;
            case "M": default:
                segments = qr.segments[version].M;
                maxDataLength = 8 * qr.dataCodewords.M[version];
                break;
            }

            // check total length
            if (dataLength <= maxDataLength) {
                break;
            }
            if (this.props.version !== "auto" || !qr.segments[++version]) {
                throw new Error("QrModel2: data is too large.");
            }
        }

        return { version, segments, dataLength, maxDataLength };
    }


    protected encodeData(data: QrSourceDataTypes[] | QrSourceDataTypes): {
            version: number, data: Uint8Array, ecLevel: "L" | "M" | "Q" | "H" } {

        // determine chunks' encoding, and encoding data.
        const chunks = this.encodeChunks(data);

        // determine symbol's version.
        const { version, segments, dataLength, maxDataLength } = this.determineSymbolVersion(chunks);

        // convert chunks to QR data structure bit stream
        let bytes: Uint8Array;
        {
            const bits: BitStreamWriter[] = [];

            // make global headers.

            // build data from chunks.
            for (const c of chunks) {
                let n = 0;
                const header = new BitStreamWriter(3);

                switch (c.type) {
                case QrDataChunkType.Number:
                    header.writeBits8(1, 4);
                    if      (version < 10) n = 10;
                    else if (version < 27) n = 12;
                    else                   n = 14;
                    header.writeBits(c.charLength, n);
                    break;
                case QrDataChunkType.Alnum:
                    header.writeBits8(2, 4);
                    if      (version < 10) n =  9;
                    else if (version < 27) n = 11;
                    else                   n = 13;
                    header.writeBits(c.charLength, n);
                    break;
                case QrDataChunkType.Binary:
                    header.writeBits8(4, 4);
                    if      (version < 10) n =  8;
                    else                   n = 16;
                    header.writeBits(c.charLength, n);
                    break;
                }

                if (0 < header.bitLength) bits.push(header);
                bits.push(c.data);
            }

            // make global footers.

            // finalize data.
            // if data bits and codewords are remained, add terminator, padding bits, padding codewords.
            if (dataLength < maxDataLength) {
                let rem = maxDataLength - dataLength;
                const fin = new BitStreamWriter(Math.ceil(rem / 8));

                // terminator
                let len = Math.min(4, rem);
                fin.writeBits8(0, len);
                rem -= len;

                // padding bits
                if ((0 < rem) && (0 !== (rem % 8))) {
                    len = rem % 8;
                    fin.writeBits8(0, len);
                    rem -= len;
                }

                // padding codewords
                for (let i = 0; 0 < rem; i++) {
                    len = Math.min(8, rem);
                    fin.writeBits8(0 === (i % 2) ? 0x00ec : 0x0011, len);
                    rem -= len;
                }

                bits.push(fin);
            }

            bytes = BitStreamWriter.concat(...bits).toBytes();
        }

        // make data codewords, generate error correction codewords.
        let totalLength = 0;
        const dataCwStack: Uint8Array[] = [];
        const   ecCwStack: Uint8Array[] = [];
        for (let i = 0, p = 0; i < segments.length; i++) {
            const repeats    = segments[i][0];
            const dataCwSize = segments[i][2];
            const ecCwSize   = segments[i][1] - dataCwSize;

            let gx: number[];
            if (gxList.length <= (ecCwSize - 1)) {
                gxList = ReedSolomon.listGx(field, gxList, ecCwSize, 0);
            }
            gx = gxList[ecCwSize - 1];
            const rs = new ReedSolomon(field, gx, 0);

            for (let j = 0; j < repeats; j++) {
                const dataCodewords = bytes.slice(p, p + dataCwSize).reverse();
                const ecCodewords   = Uint8Array.from(rs.encode(dataCodewords));
                dataCwStack.push(dataCodewords.reverse());
                ecCwStack  .push(ecCodewords.reverse());
                totalLength += dataCwSize + ecCwSize;
                p += dataCwSize;
            }
        }

        // interleaving codewords.
        const codewords = new Uint8Array(totalLength);
        {
            const stacks = [dataCwStack, ecCwStack];
            let p = 0;
            for (const stack of stacks) {
                for (let i = 0, z = true; z; i++) {
                    z = false;
                    for (let j = 0; j < stack.length; j++) {
                        if (i < stack[j].length) {
                            codewords[p++] = stack[j][i];
                            z = true;
                        }
                    }
                }
            }
        }

        const props = this.props as QrPropsNoUndefined;
        return { data: codewords, version, ecLevel: props.ecLevel };
    }

    protected applyMask(bitmap: Bitmap, funcPatternsMap: Bitmap, fn: (x: number, y: number) => boolean) {
        const nx = bitmap.width,
              ny = bitmap.width;

        for (let x = 0; x < nx; x++) {
            for (let y = 0; y < ny; y++) {
                if (0 === funcPatternsMap.get(nx - 1 - x, ny - 1 - y)) {
                    bitmap.set(nx - 1 - x, ny - 1 - y, bitmap.get(nx - 1 - x, ny - 1 - y) ^ (fn(x, y) ? 1 : 0));
                }
            }
        }
    }

    protected buildBitmap(data: Uint8Array, version: number): Bitmap {
        const width = qr.matrixSize[version];
        const nx = width,
              ny = width;
        const bitmap = new Bitmap(width, width);
        const funcPatternsMap = new Bitmap(width, width);

        {
            {
                // finder patterns + format info
                funcPatternsMap.fill(     0, ny - 9, 8, 9, 1);
                funcPatternsMap.fill(nx - 9,      0, 9, 8, 1);
                funcPatternsMap.fill(nx - 9, ny - 9, 9, 9, 1);

                // version info
                if (7 <= version) {
                    funcPatternsMap.fill(     8, ny - 6, 3, 6, 1);
                    funcPatternsMap.fill(nx - 6,      8, 6, 3, 1);
                }
            }

            // finder patterns
            for (const {px, py} of [{px: nx - 7, py: ny - 7}, {px: 0, py: ny - 7}, {px: nx - 7, py: 0}]){
                bitmap.set(px + 0, py + 0, 1);
                bitmap.set(px + 0, py + 1, 1);
                bitmap.set(px + 0, py + 2, 1);
                bitmap.set(px + 0, py + 3, 1);
                bitmap.set(px + 0, py + 4, 1);
                bitmap.set(px + 0, py + 5, 1);
                bitmap.set(px + 0, py + 6, 1);
                bitmap.set(px + 1, py + 0, 1);
                bitmap.set(px + 2, py + 0, 1);
                bitmap.set(px + 3, py + 0, 1);
                bitmap.set(px + 4, py + 0, 1);
                bitmap.set(px + 5, py + 0, 1);
                bitmap.set(px + 6, py + 0, 1);
                bitmap.set(px + 6, py + 1, 1);
                bitmap.set(px + 6, py + 2, 1);
                bitmap.set(px + 6, py + 3, 1);
                bitmap.set(px + 6, py + 4, 1);
                bitmap.set(px + 6, py + 5, 1);
                bitmap.set(px + 1, py + 6, 1);
                bitmap.set(px + 2, py + 6, 1);
                bitmap.set(px + 3, py + 6, 1);
                bitmap.set(px + 4, py + 6, 1);
                bitmap.set(px + 5, py + 6, 1);
                bitmap.set(px + 6, py + 6, 1);

                bitmap.set(px + 2, py + 2, 1);
                bitmap.set(px + 2, py + 3, 1);
                bitmap.set(px + 2, py + 4, 1);
                bitmap.set(px + 3, py + 2, 1);
                bitmap.set(px + 4, py + 2, 1);
                bitmap.set(px + 4, py + 3, 1);
                bitmap.set(px + 4, py + 4, 1);
                bitmap.set(px + 3, py + 4, 1);
                bitmap.set(px + 3, py + 3, 1);
            }

            // alignment patterns
            const aps = qr.alignmentPatterns[version];
            for (let i = 0; i < aps.length; i++) {
                const a = nx - aps[i] - 1;
                for (let j = 0; j < aps.length; j++) {
                    const b = ny - aps[j] - 1;
                    let q = true;
                    for (let x = (a - 2); x <= (a + 2); x++) {
                        for (let y = (b - 2); y <= (b + 2); y++) {
                            if (funcPatternsMap.get(x, y)) {
                                q = false;
                                break;
                            }
                        }
                        if (!q) break;
                    }
                    if (q) {
                        funcPatternsMap.fill(a - 2, b - 2, 5, 5, 1);

                        bitmap.set(a - 2, b + 2, 1);
                        bitmap.set(a - 2, b + 1, 1);
                        bitmap.set(a - 2, b    , 1);
                        bitmap.set(a - 2, b - 1, 1);
                        bitmap.set(a - 2, b - 2, 1);
                        bitmap.set(a + 2, b + 2, 1);
                        bitmap.set(a + 2, b + 1, 1);
                        bitmap.set(a + 2, b    , 1);
                        bitmap.set(a + 2, b - 1, 1);
                        bitmap.set(a + 2, b - 2, 1);
                        bitmap.set(a + 1, b - 2, 1);
                        bitmap.set(a + 1, b + 2, 1);
                        bitmap.set(a    , b + 2, 1);
                        bitmap.set(a    , b    , 1);
                        bitmap.set(a    , b - 2, 1);
                        bitmap.set(a - 1, b - 2, 1);
                        bitmap.set(a - 1, b + 2, 1);
                    }
                }
            }

            // timing pattern
            funcPatternsMap.fill(     8, ny - 7, nx - 17,       1, 1);
            funcPatternsMap.fill(nx - 7,      8,       1, ny - 17, 1);

            // timing pattern
            for (let i = 8; i <= ny - 9; i += 2) {
                bitmap.set(nx - 7, i, 1);
                bitmap.set(i, ny - 7, 1);
            }
        }

        // place codewords
        {
            let cx =  1;
            let cy = -1;
            let b2t =  true; // bottom to top
            let odd = false;

            for (let i = 0; i < data.length; i++) {
                for (let j = 7; j >= 0; j--) {
                    for (; ; ) {
                        if (odd) {
                            cx += 1;
                        } else if ((((ny - 1) === cy) && b2t) || ((0 === cy) && (!b2t))) {
                            if (cx === (nx - 8)) cx += 2;
                            else                 cx += 1;
                            b2t = !b2t;
                        } else {
                            cx -= 1;
                            if (b2t) cy += 1;
                            else     cy -= 1;
                        }
                        odd = !odd;

                        // determine
                        if (0 === funcPatternsMap.get(cx, cy)) {
                            bitmap.set(cx, cy, (data[i] >>> j) & 0x01);
                            break;
                        } else {
                            continue;
                        }
                    }
                }
            }
        }

        // place version info
        if (7 <= version) {
            // gx=0x1f25 BCH Code is built on GF(2^4). but BCH#encode() is not use arithmetic on GF.
            const vi = ((version << 12) | new BCH(field, 0x1f25).encode(version));

            bitmap.set(nx - 1, 10, (vi >>>  0) & 1);
            bitmap.set(nx - 1,  9, (vi >>>  1) & 1);
            bitmap.set(nx - 1,  8, (vi >>>  2) & 1);
            bitmap.set(nx - 2, 10, (vi >>>  3) & 1);
            bitmap.set(nx - 2,  9, (vi >>>  4) & 1);
            bitmap.set(nx - 2,  8, (vi >>>  5) & 1);
            bitmap.set(nx - 3, 10, (vi >>>  6) & 1);
            bitmap.set(nx - 3,  9, (vi >>>  7) & 1);
            bitmap.set(nx - 3,  8, (vi >>>  8) & 1);
            bitmap.set(nx - 4, 10, (vi >>>  9) & 1);
            bitmap.set(nx - 4,  9, (vi >>> 10) & 1);
            bitmap.set(nx - 4,  8, (vi >>> 11) & 1);
            bitmap.set(nx - 5, 10, (vi >>> 12) & 1);
            bitmap.set(nx - 5,  9, (vi >>> 13) & 1);
            bitmap.set(nx - 5,  8, (vi >>> 14) & 1);
            bitmap.set(nx - 6, 10, (vi >>> 15) & 1);
            bitmap.set(nx - 6,  9, (vi >>> 16) & 1);
            bitmap.set(nx - 6,  8, (vi >>> 17) & 1);

            bitmap.set(10, ny - 1, (vi >>>  0) & 1);
            bitmap.set( 9, ny - 1, (vi >>>  1) & 1);
            bitmap.set( 8, ny - 1, (vi >>>  2) & 1);
            bitmap.set(10, ny - 2, (vi >>>  3) & 1);
            bitmap.set( 9, ny - 2, (vi >>>  4) & 1);
            bitmap.set( 8, ny - 2, (vi >>>  5) & 1);
            bitmap.set(10, ny - 3, (vi >>>  6) & 1);
            bitmap.set( 9, ny - 3, (vi >>>  7) & 1);
            bitmap.set( 8, ny - 3, (vi >>>  8) & 1);
            bitmap.set(10, ny - 4, (vi >>>  9) & 1);
            bitmap.set( 9, ny - 4, (vi >>> 10) & 1);
            bitmap.set( 8, ny - 4, (vi >>> 11) & 1);
            bitmap.set(10, ny - 5, (vi >>> 12) & 1);
            bitmap.set( 9, ny - 5, (vi >>> 13) & 1);
            bitmap.set( 8, ny - 5, (vi >>> 14) & 1);
            bitmap.set(10, ny - 6, (vi >>> 15) & 1);
            bitmap.set( 9, ny - 6, (vi >>> 16) & 1);
            bitmap.set( 8, ny - 6, (vi >>> 17) & 1);
        }

        // masking
        let maskNo = 0;
        {
            let maskScore = Number.MAX_SAFE_INTEGER;

            // for each mask procs
            for (const mask of masks) {
                // mask
                this.applyMask(bitmap, funcPatternsMap, mask.fn);

                // calculate score
                const score = this.evaluteMask(bitmap);
                if (score < maskScore) {
                    maskNo = mask.index;
                    maskScore = score;
                }

                // unmask
                this.applyMask(bitmap, funcPatternsMap, mask.fn);
            }

            // mask
            this.applyMask(bitmap, funcPatternsMap, masks[maskNo].fn);
        }

        // place format info
        {
            const props = this.props as QrPropsNoUndefined;
            const ecLevel = ecLevelMap.get(props.ecLevel);
            if (ecLevel === void 0) {
                throw new Error(`Qr#buildBitmap: bad ecLevel '${props.ecLevel}' is specified.`);
            }
            let fi = (ecLevel << 3) | masks[maskNo].index;

            // gx=0x0537 BCH Code is built on GF(2^4). but BCH#encode() is not use arithmetic on GF.
            fi = ((fi << 10) | new BCH(field, 0x0537).encode(fi)) ^ 0x5412;

            for (let i = 0; i <= 7; i++) {
                bitmap.set(i          , ny - 9, (fi >>> i) & 1);
            }
            {
                bitmap.set(    nx -  8, ny - 9, (fi >>> 8) & 1);
            }
            for (let i = 9; i <= 14; i++) {
                bitmap.set(i + nx - 15, ny - 9, (fi >>> i) & 1);
            }

            for (let i = 0; i <= 5; i++) {
                bitmap.set(nx - 9, ny - 1 - i, (fi >>> i) & 1);
            }
            for (let i = 6; i <= 7; i++) {
                bitmap.set(nx - 9, ny - 2 - i, (fi >>> i) & 1);
            }
            {
                bitmap.set(nx - 9,  7        ,              1);
            }
            for (let i = 8; i <= 14; i++) {
                bitmap.set(nx - 9, 14     - i, (fi >>> i) & 1);
            }
        }

        return bitmap;
    }

    protected drawBitmap(canvas: SvgCanvas, bitmap: Bitmap) {
        // bitmap's coodinate origin is right-bottom.
        const c = this.props.cellSize as number;
        const nx = bitmap.width,
              ny = bitmap.height;

        for (let i = 0; i < nx; i++) {
            for (let j = 0; j < ny; j++) {
                if (bitmap.get(i, j)) canvas.rect((nx - 1 - i) * c, (ny - 1 - j) * c, c, c);
            }
        }

        canvas.fill();
        canvas.beginPath();
    }
}
