/*
    Предрейсовые осмотры - мобильное приложение
    Генератор иконок приложения из одного PNG
*/

'use strict';

//---------------------
//Подключение библиотек
//---------------------

const fs = require('fs');
const path = require('path');
const zlib = require('zlib');

//---------
//Константы
//---------

const APP_ROOT = path.join(__dirname, '..');
const LOGO_PATH = path.join(APP_ROOT, 'assets', 'icons', 'logo.png');

const ANDROID_SIZES = Object.freeze([
    Object.freeze({ dir: 'mipmap-mdpi', size: 48 }),
    Object.freeze({ dir: 'mipmap-hdpi', size: 72 }),
    Object.freeze({ dir: 'mipmap-xhdpi', size: 96 }),
    Object.freeze({ dir: 'mipmap-xxhdpi', size: 144 }),
    Object.freeze({ dir: 'mipmap-xxxhdpi', size: 192 })
]);

const IOS_ICONS = Object.freeze([
    Object.freeze({ filename: 'icon-20@2x.png', size: 40 }),
    Object.freeze({ filename: 'icon-20@3x.png', size: 60 }),
    Object.freeze({ filename: 'icon-29@2x.png', size: 58 }),
    Object.freeze({ filename: 'icon-29@3x.png', size: 87 }),
    Object.freeze({ filename: 'icon-40@2x.png', size: 80 }),
    Object.freeze({ filename: 'icon-40@3x.png', size: 120 }),
    Object.freeze({ filename: 'icon-60@2x.png', size: 120 }),
    Object.freeze({ filename: 'icon-60@3x.png', size: 180 }),
    Object.freeze({ filename: 'icon-1024.png', size: 1024 })
]);

const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ANDROID_RES_DIR = path.join(APP_ROOT, 'android', 'app', 'src', 'main', 'res');
const IOS_APP_ICON_DIR = path.join(APP_ROOT, 'ios', 'app', 'Images.xcassets', 'AppIcon.appiconset');

const U32_MAX = 4294967296;

//-----------
//Тело модуля
//-----------

//Приведение числа к беззнаковому 32-битному (без побитовых операторов)
function u32(x) {
    return ((x % U32_MAX) + U32_MAX) % U32_MAX;
}

//Побитовое XOR для двух 32-битных беззнаковых (через арифметику, для соответствия ESLint)
function xor32(a, b) {
    const au = u32(a);
    const bu = u32(b);
    let r = 0;
    let p = 1;
    for (let i = 0; i < 32; i++) {
        if (Math.floor(au / p) % 2 !== Math.floor(bu / p) % 2) r += p;
        p *= 2;
    }
    return u32(r);
}

//Байт в диапазоне 0..255 (эквивалент & 0xff для суммы/разности байтов)
function byteClamp(v) {
    return ((v % 256) + 256) % 256;
}

//Таблица CRC32 для PNG-чанков (полином IEEE, без побитовых операторов)
const CRC_TABLE = (() => {
    const table = new Uint32Array(256);
    const poly = 0xedb88320;
    for (let n = 0; n < 256; n++) {
        let c = n;
        for (let k = 0; k < 8; k++) {
            const low = c % 2;
            c = Math.floor(u32(c) / 2);
            if (low) c = xor32(poly, c);
        }
        table[n] = u32(c);
    }
    return table;
})();

//CRC32 от буфера (тип чанка + данные), для проверки и записи PNG. Индекс таблицы — младший байт (crc XOR byte).
function crc32(buffer, start = 0, length = buffer.length - start) {
    let crc = 0xffffffff;
    const end = start + length;
    for (let i = start; i < end; i++) {
        const lo = xor32(crc, buffer[i]);
        const indexByte = u32(lo) % 256;
        crc = xor32(CRC_TABLE[indexByte], Math.floor(u32(crc) / 256));
    }
    return u32(xor32(crc, 0xffffffff));
}

//Чтение PNG: файл → RGBA (Uint8Array, по строкам, 4 байта на пиксель). Поддержка RGB/RGBA 8-bit.
function readPng(filePath) {
    const data = fs.readFileSync(filePath);
    let offset = 0;

    if (data.length < 8 || !PNG_SIGNATURE.equals(data.subarray(0, 8))) {
        throw new Error(`Invalid PNG signature: ${filePath}`);
    }
    offset = 8;

    let width = 0;
    let height = 0;
    let bitDepth = 0;
    let colorType = 0;
    let interlace = 0;
    const idatChunks = [];

    while (offset < data.length) {
        const length = data.readUInt32BE(offset);
        const type = data.toString('ascii', offset + 4, offset + 8);
        const chunkStart = offset + 8;
        const chunkEnd = chunkStart + length;

        if (chunkEnd + 4 > data.length) throw new Error('PNG truncated');
        const crcExpected = data.readUInt32BE(chunkEnd);
        const typeAndDataStart = offset + 4;
        const crcInput = data.subarray(typeAndDataStart, typeAndDataStart + 4 + length);
        const crcGot = crc32(crcInput);
        if (crcGot !== crcExpected) throw new Error(`PNG chunk CRC mismatch: ${type}`);

        offset = chunkEnd + 4;

        if (type === 'IHDR') {
            if (length < 13) throw new Error('IHDR too short');
            width = data.readUInt32BE(chunkStart);
            height = data.readUInt32BE(chunkStart + 4);
            bitDepth = data[chunkStart + 8];
            colorType = data[chunkStart + 9];
            interlace = data[chunkStart + 12];
        } else if (type === 'IDAT') {
            idatChunks.push(data.subarray(chunkStart, chunkEnd));
        } else if (type === 'IEND') {
            break;
        }
    }

    if (interlace !== 0) throw new Error('Interlaced PNG is not supported');
    if (bitDepth !== 8) throw new Error('Only 8-bit PNG is supported');
    if (colorType !== 2 && colorType !== 6) {
        throw new Error('Only RGB or RGBA PNG is supported (color type 2 or 6)');
    }

    const bytesPerPixel = colorType === 6 ? 4 : 3;
    const rawSize = (1 + width * bytesPerPixel) * height;
    const concatenated = Buffer.concat(idatChunks);
    const inflated = zlib.inflateSync(concatenated, { chunkSize: rawSize + 1024 });
    if (inflated.length < rawSize) throw new Error('PNG decompressed size too small');

    // Снятие фильтров строк и приведение к RGBA
    const rgba = new Uint8Array(width * height * 4);
    const stride = width * bytesPerPixel;
    let prev = null;

    for (let y = 0; y < height; y++) {
        const rowStart = y * (1 + stride);
        const filter = inflated[rowStart];
        const raw = inflated.subarray(rowStart + 1, rowStart + 1 + stride);
        const out = new Uint8Array(stride);

        for (let x = 0; x < stride; x++) {
            const left = x >= bytesPerPixel ? out[x - bytesPerPixel] : 0;
            const up = prev ? prev[x] : 0;
            const upLeft = prev && x >= bytesPerPixel ? prev[x - bytesPerPixel] : 0;

            let v = raw[x];
            if (filter === 1) v = byteClamp(v + left);
            else if (filter === 2) v = byteClamp(v + up);
            else if (filter === 3) v = byteClamp(v + Math.floor((left + up) / 2));
            else if (filter === 4) v = byteClamp(v + paeth(left, up, upLeft));
            out[x] = v;
        }
        prev = out;

        for (let x = 0; x < width; x++) {
            const i = x * bytesPerPixel;
            const o = (y * width + x) * 4;
            rgba[o] = out[i];
            rgba[o + 1] = out[i + 1];
            rgba[o + 2] = out[i + 2];
            rgba[o + 3] = bytesPerPixel === 4 ? out[i + 3] : 255;
        }
    }

    return { width, height, rgba };
}

//Предиктор Paeth для фильтра PNG (без побитовых операций)
function paeth(a, b, c) {
    const p = a + b - c;
    const pa = Math.abs(p - a);
    const pb = Math.abs(p - b);
    const pc = Math.abs(p - c);
    if (pa <= pb && pa <= pc) return a;
    if (pb <= pc) return b;
    return c;
}

//Масштабирование RGBA билинейной интерполяцией
function resizeBilinear(srcWidth, srcHeight, srcRgba, dstWidth, dstHeight) {
    const dst = new Uint8Array(dstWidth * dstHeight * 4);
    const src = srcRgba;

    for (let dy = 0; dy < dstHeight; dy++) {
        const sy = (dy + 0.5) * (srcHeight / dstHeight) - 0.5;
        const sy0 = Math.max(0, Math.floor(sy));
        const sy1 = Math.min(srcHeight - 1, sy0 + 1);
        const fy = sy - sy0;

        for (let dx = 0; dx < dstWidth; dx++) {
            const sx = (dx + 0.5) * (srcWidth / dstWidth) - 0.5;
            const sx0 = Math.max(0, Math.floor(sx));
            const sx1 = Math.min(srcWidth - 1, sx0 + 1);
            const fx = sx - sx0;

            const i00 = (sy0 * srcWidth + sx0) * 4;
            const i10 = (sy0 * srcWidth + sx1) * 4;
            const i01 = (sy1 * srcWidth + sx0) * 4;
            const i11 = (sy1 * srcWidth + sx1) * 4;

            const o = (dy * dstWidth + dx) * 4;
            for (let ch = 0; ch < 4; ch++) {
                const v00 = src[i00 + ch];
                const v10 = src[i10 + ch];
                const v01 = src[i01 + ch];
                const v11 = src[i11 + ch];
                const top = v00 * (1 - fx) + v10 * fx;
                const bot = v01 * (1 - fx) + v11 * fx;
                dst[o + ch] = Math.round(top * (1 - fy) + bot * fy);
            }
        }
    }
    return dst;
}

//Кодирование RGBA в буфер PNG (фильтр None, deflate)
function writePngBuffer(width, height, rgba) {
    const stride = width * 4;
    const rawRows = [];
    for (let y = 0; y < height; y++) {
        const row = Buffer.alloc(1 + stride);
        row[0] = 0; // фильтр None
        for (let x = 0; x < width; x++) {
            const i = (y * width + x) * 4;
            row[1 + x * 4] = rgba[i];
            row[2 + x * 4] = rgba[i + 1];
            row[3 + x * 4] = rgba[i + 2];
            row[4 + x * 4] = rgba[i + 3];
        }
        rawRows.push(row);
    }
    const rawData = Buffer.concat(rawRows);
    const compressed = zlib.deflateSync(rawData, { level: 9 });

    function writeChunk(out, type, data) {
        const len = data ? data.length : 0;
        const buf = Buffer.alloc(12 + len);
        buf.writeUInt32BE(len, 0);
        buf.write(type, 4, 4, 'ascii');
        if (data) data.copy(buf, 8);
        const crc = crc32(Buffer.concat([Buffer.from(type, 'ascii'), data || Buffer.alloc(0)]));
        buf.writeUInt32BE(crc, 8 + len);
        out.push(buf);
    }

    const ihdr = Buffer.alloc(13);
    ihdr.writeUInt32BE(width, 0);
    ihdr.writeUInt32BE(height, 4);
    ihdr[8] = 8; // глубина цвета
    ihdr[9] = 6; // тип цвета RGBA
    ihdr[10] = 0; // сжатие deflate
    ihdr[11] = 0; // фильтр адаптивный
    ihdr[12] = 0; // без чересстрочности

    const out = [PNG_SIGNATURE];
    writeChunk(out, 'IHDR', ihdr);
    writeChunk(out, 'IDAT', compressed);
    writeChunk(out, 'IEND', null);
    return Buffer.concat(out);
}

//Создание каталога при необходимости
function ensureDir(dirPath) {
    if (!fs.existsSync(dirPath)) {
        fs.mkdirSync(dirPath, { recursive: true });
    }
}

//Генерация всех иконок: один раз читаем логотип, ресайзим и пишем в Android/iOS
async function generate() {
    if (!fs.existsSync(LOGO_PATH)) {
        const err = new Error(`Logo not found: ${LOGO_PATH}`);
        err.code = 'ENOENT';
        throw err;
    }

    const logo = readPng(LOGO_PATH);
    const { width: w, height: h, rgba } = logo;

    for (const { dir, size } of ANDROID_SIZES) {
        const dirPath = path.join(ANDROID_RES_DIR, dir);
        ensureDir(dirPath);
        const resized = resizeBilinear(w, h, rgba, size, size);
        const png = writePngBuffer(size, size, resized);
        const base = path.join(dirPath, 'ic_launcher.png');
        const round = path.join(dirPath, 'ic_launcher_round.png');
        await Promise.all([fs.promises.writeFile(base, png), fs.promises.writeFile(round, png)]);
        console.log('Android', dir, `${size}x${size}`);
    }

    for (const { filename, size } of IOS_ICONS) {
        const outPath = path.join(IOS_APP_ICON_DIR, filename);
        const resized = resizeBilinear(w, h, rgba, size, size);
        const png = writePngBuffer(size, size, resized);
        await fs.promises.writeFile(outPath, png);
        console.log('iOS', filename, `${size}x${size}`);
    }

    console.log('Done. App icon set from', LOGO_PATH);
}

//----------------
//Точка входа
//----------------

generate().catch(err => {
    console.error(err.message || err);
    process.exit(1);
});
