const {open} = require("fs/promises");
const {argv} = require("process");

// ACF文件格式：
// 0-3:ACFv ACF为固定字符串，v表示版本号，从1开始
// 4-7:font bounding 来自bdf文件
// 8-9: fragment items size 字体数据开始的位置
// 10-:fragment items 分段描述数据，一个分段的unicode是连续的
//
// 分段描述数据内容
// 0-1:start unicode 本分段第一个unicode
// 2-3:end unicode 本分段最后一个unicode
// 5: padding size for unicode 本分段每个unicode的字体数据占用的字节长度
// 6: bbxd count in this fragment 本分段总共用到的bbxd种类
// other: 4*(bbxd count) 本分段用到的所有bbxd的原始数据
//
// bbxd数据格式：
// 4个6字节的整数(-32 ~ 31)，表示BBX对应的内容，加上一个6字节的整数表示dwidth的第一个数据
//
// 字体数据
// 同一个fragment里面的所有unicode长度都是padding size，如果实际数据少的会补齐0
// 如果fragment的bbxd count为1，则字体数据就是直接boundingbox的所有点阵数据
// 如果fragment的bbxd count大于1，则字体数据是1字节的bbxd索引，后接boundingbox的所有点阵数据

const helper = () => {
    const setBitAt = (bitarr, pos) => {
        let byteIndex = Math.floor( pos / 8 );
        let bitOffset = pos % 8;
        bitarr[ byteIndex ] |= 1 << bitOffset;
    }
    const testBit = (byte, pos) => byte & ( 1 << pos);
    const putS6 = (arr, val, pos) => {
        // sign bit
        if( val < 0 ) {
            setBitAt( arr, pos );
            val = -val;
        }
        ++pos;

        // digits
        for( let i=1<<4; i > 0; i>>=1, pos++ ){
            if( val & i ) {
                setBitAt(arr, pos);
            }
        }
    }
    return {setBitAt, testBit, putS6}
}

const createDescript = () => {
    let numChars, boundingbox;
    let glyphs = []
    let charGlyph = {}
    let fragments = []
    
    const apply = async ( cmd, params ) => {
        if( cmd == "chars" ) {
            [numChars] = params.map( n => Number(n) );
        }
        else if( cmd == "fontboundingbox" ) {
            boundingbox = params.map( n => Number(n) );
        }
        else if( cmd == "startchar" ){
            charGlyph = {};

            if( fragments.length == 0 ){
                fragments.push({ padding: 0, bbxd: new Map() });
            }
        }
        else if( cmd == "encoding" ){
            charGlyph.code = Number(params[0]);
        }
        else if( cmd == "dwidth" ){
            charGlyph.dwidth = params.map( n => Number(n) );
        }
        else if( cmd == "bbx" ){
            charGlyph.bbx = params.map( n => Number(n) );
        }
        else if( cmd == "bitmap" ){
            charGlyph.recoding = true;
            charGlyph.data = [];
        }
        else if( cmd == "endchar" ){
            const {recoding, code, ...value} = charGlyph;
            const [w, h] = charGlyph.bbx;

            let fragment = fragments.at(-1);
            if( !("start" in fragment) ){
                fragment.start = code;
            }
            else if( glyphs.at(-1).code + 1 != code ){// break
                fragment.end = glyphs.at(-1).code;
                
                // new fragment
                fragment = { start: code, padding: 0, bbxd: new Map() };
                fragments.push( fragment );
            }

            if( w*h > fragment.padding ){
                fragment.padding = w*h;
            }

            const key = JSON.stringify( [...value.bbx, value.dwidth[0]] )
            if( !fragment.bbxd.has(key) ){
                fragment.bbxd.set( key, fragment.bbxd.size );
            }

            charGlyph = {};
            glyphs.push( {...value, code, fragment} );
        }
        else if( cmd == "endfont" ){
            const fragment = fragments.at(-1);
            fragment.end = glyphs.at(-1).code;
        }
        else if( charGlyph.recoding ){
            charGlyph.data.push( parseInt(cmd, 16) );
        }
    }
    const save = async file => {
        const { setBitAt, testBit, putS6 } = helper();
        const packHead = () => {
            const buffer = new ArrayBuffer(6);
            const view = new DataView(buffer);
            view.setUint16(0, numChars, true);
            boundingbox.forEach( (c, i) => view.setInt8(2+i, c) )
            return new Uint8Array(buffer);
        }
        const packHeadV2 = fragSize => {
            const buffer = new ArrayBuffer(10);
            const view = new DataView(buffer);
            view.setUint8(0, "A".charCodeAt())
            view.setUint8(1, "C".charCodeAt())
            view.setUint8(2, "F".charCodeAt())
            view.setUint8(3, 2)//v2
            boundingbox.forEach( (c, i) => view.setInt8(4+i, c) )
            view.setUint16(8, fragSize, true)
            return new Uint8Array(buffer);
        }
        const packGlyphs = () => {
            return glyphs.map( glyph => [packChar(glyph), packPixel(glyph), glyph] )
        }
        const packChar = glyph => {
            const buffer = new Uint8Array(4);
            const bitwid = 6;
            glyph.bbx.forEach( (c,i) => putS6(buffer, c, bitwid*i) );
            putS6( buffer, glyph.dwidth[0], bitwid*4 );
            return buffer;
        }
        const packPixel = glyph => {
            const [w, h] = glyph.bbx;
            const {padding} = glyph.fragment;
            const nbyte = Math.ceil( padding / 8 );
            const buffer = new Uint8Array( nbyte );
            const base = 8 * ( (7+w) >> 3 ) - 1;
            for( let j = 0; j < h; ++j ){
                let linepixel = glyph.data[j];
                for( let i = 0; i < w; ++i ){
                    if( testBit(linepixel, base-i) ){
                        setBitAt( buffer, j*w+i )
                    }
                }
            }
            return buffer;
        }
        const packIndex = fonts => {
            const buffer = new ArrayBuffer( numChars * 5 );
            const view = new DataView( buffer );
            let offset = 0;
            glyphs.forEach( (c, i) => {
                view.setUint16( i*5, c.code, true );
                view.setUint8( i*5 + 2, offset >> 16 );
                view.setUint8( i*5 + 3, offset & 0xFF );
                view.setUint8( i*5 + 4, (offset & 0xFFFF) >> 8 );
                offset += 4 + fonts[i][1].length;
            })
            return new Uint8Array(buffer);
        }
        const packFragments = () => {
            const bitwid = 6;
            const serials = fragments.map( frag => {
                const buffer = new ArrayBuffer( 6+4*frag.bbxd.size );
                const view = new DataView( buffer );
                const nbyte = Math.ceil( frag.padding / 8 );
                view.setUint16( 0, frag.start, true );
                view.setUint16( 2, frag.end, true );
                view.setUint8( 4, frag.bbxd.size > 1 ? 1+nbyte : nbyte );
                view.setUint8( 5, frag.bbxd.size );
                
                frag.bbxd.forEach( (idx, key) => {
                    const arr = new Uint8Array(buffer, 6+idx*4, 4);
                    const embeded = JSON.parse( key );
                    embeded.forEach( (c,i)=>putS6(arr, c, bitwid*i) )
                } )
                return new Uint8Array(buffer);
            })
            return serials;
        }

        /*
         * old code: pack for acfv1
        const head = packHead();
        const glyph = packGlyphs();
        const index = packIndex( glyph );

        let out = await open(file, "w");
        await out.write(head);
        await out.write(index);
        let jobs = Promise.resolve();
        glyph.forEach( ([char, pixel]) => {
            const combine = Uint8Array.from([...char, ...pixel]) 
            jobs = jobs.then( () => out.write(combine) )
        } )
        await jobs;
        await out.close();
        */

        const index = packFragments();
        const head = packHeadV2( index.reduce( (r,b)=>r+b.length, 0 ) );
        const glyph = packGlyphs();

        let out = await open(file, "w");
        await out.write( head );
        let jobs = Promise.resolve();
        index.forEach( buffer => {
            jobs = jobs.then( () => out.write(buffer) )
        } );
        await jobs;

        jobs = Promise.resolve();
        glyph.forEach( ([char, pixel, glyph]) => {
            const {fragment} = glyph;
            if( fragment.bbxd.size == 1 ) jobs = jobs.then( () => out.write(pixel) )
            else {
                const key = JSON.stringify( [...glyph.bbx, glyph.dwidth[0]] )
                const combine = Uint8Array.from( [fragment.bbxd.get(key), ...pixel] )
                jobs = jobs.then( () => out.write(combine) )
            }
        } )
        await jobs;
        await out.close();
    }
    return { apply, save }
}

const createReader = file => {
    const newline = "\n".charCodeAt();

    let buffer = new Uint8Array(400);
    let cursor = buffer.length; 
    let line = ""
    const readline = async () => {
        if( cursor == buffer.length ){
            cursor = 0;
            await file.read( buffer, cursor, buffer.length )
        }

        let chars = []
        let fullline = null;
        while( cursor < buffer.length ){
            const char = buffer[cursor++];
            if( char == newline ) {
                fullline = `${line}${String.fromCharCode(...chars)}`;
                break;
            }

            chars.push( char )
        }

        if( fullline ){
            line = "";          // reset line
            return fullline.length > 0 ? fullline.split(" ").map( (word,i) => i==0 ? word.toLowerCase():word ) : [null];
        }
        else {
            line = `${line}${String.fromCharCode(...chars)}`;
            return await readline()
        }
    }
    return readline;
}

const convert = async fname => {
    let bdf = await open( fname, "r" );

    const readline = createReader( bdf );
    const bdfdesc  = createDescript();

    while( true ){
        let [cmd, ...params] = await readline();

        await bdfdesc.apply( cmd, params )
        
        if( cmd == "endfont" ) break;
    }

    await bdf.close();
    await bdfdesc.save( fname.replace(".bdf", ".acf") );
}

const start = async params => {
    if( params.length < 1 ){
        console.log("Usage: buildact file [file2] [...]")
        return;
    }

    const formatter = new Intl.DateTimeFormat("zh-CN", {dateStyle: "short", timeStyle: "medium", timeZone: "Asia/Shanghai"})
    globalThis.debug = log => console.log( `[DEBUG][${formatter.format(new Date())}]${log}` )
    await Promise.all( params.map( file => convert(file) ) )
}

start( argv.slice(2) );