--[[ Lua Image save
/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2017 William Wilgus
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
]]
-- save(img, path/name)
-- bmp saving derived from rockbox - screendump.c
-- bitdepth is limited by the device
-- eg. device displays greyscale, rgb images are saved greyscale
if not rb.lcd_framebuffer then rb.splash(rb.HZ, "No Support!") return nil end

do
    local rocklib_image = getmetatable(rb.lcd_framebuffer())

    -- internal constants
    local _NIL = nil -- _NIL placeholder
    local _points  = rocklib_image.points

    -- saves img to file: name
    return function(img, name)
        local file
        local bbuffer = {} -- concat buffer for s_bytes
        local fbuffer = {} -- concat buffer for file writes, reused

        local function s_bytesLE(bits, value)
            -- bits must be multiples of 8 (sizeof byte)
            local byte
            local nbytes = bit.rshift(bits, 3)
            for b = 1, nbytes do
                if value > 0 then
                    byte  = value % 256
                    value = (value - byte) / 256
                else
                    byte = 0
                end
                bbuffer[b] = string.char(byte)
            end
            return table.concat(bbuffer, _NIL, 1, nbytes)
        end

        local function s_bytesBE(bits, value)
            -- bits must be multiples of 8 (sizeof byte)
            local byte
            local nbytes = bit.rshift(bits, 3)
            for b = nbytes, 1, -1 do
                if value > 0 then
                    byte  = value % 256
                    value = (value - byte) / 256
                else
                    byte = 0
                end
                bbuffer[b] = string.char(byte)
            end
            return table.concat(bbuffer, _NIL, 1, nbytes)
        end

        local cmp = {["r"] =  function(c) return bit.band(bit.rshift(c, 16), 0xFF) end,
                     ["g"] =  function(c) return bit.band(bit.rshift(c, 08), 0xFF) end,
                     ["b"] =  function(c) return bit.band(c, 0xFF) end}

        local function bmp_color(color)
            return s_bytesLE(8, cmp.b(color))..
                   s_bytesLE(8, cmp.g(color))..
                   s_bytesLE(8, cmp.r(color))..
                   s_bytesLE(8, 0) .. ""
        end -- c_cmp(color, c.r))

        local function bmp_color_mix(c1, c2, num, den)
            -- mixes c1 and c2 as ratio of numerator / denominator
            -- used 2x each save results
            local bc1, gc1, rc1 = cmp.b(c1), cmp.g(c1), cmp.r(c1)

            return s_bytesLE(8, cmp.b(c2) - bc1 * num / den + bc1)..
                   s_bytesLE(8, cmp.g(c2) - gc1 * num / den + gc1)..
                   s_bytesLE(8, cmp.r(c2) - rc1 * num / den + rc1)..
                   s_bytesLE(8, 0) .. ""
        end

        local w, h = img:width(), img:height()
        local depth  = tonumber(img:__tostring(6)) -- RLI_INFO_DEPTH   = 0x6
        local format = tonumber(img:__tostring(7)) -- RLI_INFO_FORMAT  = 0x7

        local bpp, bypl -- bits per pixel, bytes per line
        -- bypl, pad rows to a multiple of 4 bytes
        if depth <= 4 then
            bpp = 8 -- 256 color image
            bypl = (w + 3)
        elseif depth <= 16 then
            bpp = 16
            bypl = (w * 2 + 3)
        elseif depth <= 24 then
            bpp = 24
            bypl = (w * 3 + 3)
        else
            bpp = 32
            bypl = (w * 4 + 3)
        end

        local linebytes = bit.band(bypl, bit.bnot(3))

        local bytesperpixel = bit.rshift(bpp, 3)
        local headersz = 54
        local imgszpad = h * linebytes

        local compression, n_colors = 0, 0
        local h_ppm, v_ppm = 0x00000EC4, 0x00000EC4 --Pixels Per Meter ~ 96 dpi

        if depth == 16 then
            compression = 3 -- BITFIELDS
            n_colors    = 3
        elseif depth <= 8 then
            n_colors = bit.lshift(1, depth)
        end

        headersz = headersz + (4 * n_colors)

        file = io.open('/' .. name, "w+") -- overwrite, rb ignores the 'b' flag

        if not file then
            rb.splash(rb.HZ, "Error opening /" .. name)
            return
        end
        -- create a bitmap header 'rope' with image details -- concatenated at end
        local bmpheader = fbuffer

              bmpheader[01] = "BM"
              bmpheader[02] = s_bytesLE(32, headersz + imgszpad)
              bmpheader[03] = "\0\0\0\0" -- WORD reserved 1 & 2
              bmpheader[04] = s_bytesLE(32, headersz) -- BITMAPCOREHEADER size
              bmpheader[05] = s_bytesLE(32, 40) -- BITMAPINFOHEADER size

              bmpheader[06] = s_bytesLE(32, w)
              bmpheader[07] = s_bytesLE(32, h)
              bmpheader[08] = "\1\0" -- WORD color planes ALWAYS 1
              bmpheader[09] = s_bytesLE(16, bpp) -- bits/pixel
              bmpheader[10] = s_bytesLE(32, compression)
              bmpheader[11] = s_bytesLE(32, imgszpad)
              bmpheader[12] = s_bytesLE(32, h_ppm) -- biXPelsPerMeter
              bmpheader[13] = s_bytesLE(32, v_ppm) -- biYPelsPerMeter
              bmpheader[14] = s_bytesLE(32, n_colors)
              bmpheader[15] = s_bytesLE(32, n_colors)

        -- Color Table (#n_colors entries)
        if depth == 1 then -- assuming positive display
            bmpheader[#bmpheader + 1] = bmp_color(0xFFFFFF)
            bmpheader[#bmpheader + 1] = bmp_color(0x0)
        elseif depth == 2 then
            bmpheader[#bmpheader + 1] = bmp_color(0xFFFFFF)
            bmpheader[#bmpheader + 1] = bmp_color_mix(0xFFFFFF, 0, 1, 3)
            bmpheader[#bmpheader + 1] = bmp_color_mix(0xFFFFFF, 0, 2, 3)
            bmpheader[#bmpheader + 1] = bmp_color(0x0)
        elseif depth == 16 then
            if format == 555 then
                -- red bitfield mask
                bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x00007C00)
                -- green bitfield mask
                bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x000003E0)
                -- blue bitfield mask
                bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x0000001F)
			else --565
                -- red bitfield mask
                bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x0000F800)
                -- green bitfield mask
                bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x000007E0)
                -- blue bitfield mask
                bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x0000001F)
            end
        end

        file:write(table.concat(fbuffer))-- write the header to the file now
        for i=1, #fbuffer do fbuffer[i] = _NIL end -- reuse table

        local imgdata = fbuffer
        -- pad rows to a multiple of 4 bytes
        local bytesleft = linebytes - (bytesperpixel * w)
        local t_data = {}
        local fs_bytes_E = s_bytesLE -- default save in Little Endian

        if format == 3553 then -- RGB565SWAPPED
            fs_bytes_E = s_bytesBE -- Saves in Big Endian
        end

        -- Bitmap lines start at bottom unless biHeight is negative
        for point in _points(img, 1, h, w + bytesleft, 1, 1, 1, true) do
            imgdata[#imgdata + 1] = fs_bytes_E(bpp, point or 0)

            if #fbuffer >= 31 then -- buffered write, increase # for performance
                file:write(table.concat(fbuffer))
                for i=1, #fbuffer do fbuffer[i] = _NIL end -- reuse table
            end

        end
        file:write(table.concat(fbuffer)) --write leftovers to file
        fbuffer = _NIL

        file:close()
    end -- save(img, name)
end
