/* sane - Scanner Access Now Easy.

   Copyright (C) 2003, 2004 Henning Meier-Geinitz <henning@meier-geinitz.de>
   Copyright (C) 2004, 2005 Gerhard Jaeger <gerhard@gjaeger.de>
   Copyright (C) 2004-2016 Stéphane Voltz <stef.dev@free.fr>
   Copyright (C) 2005-2009 Pierre Willenbrock <pierre@pirsoft.dnsalias.org>
   Copyright (C) 2006 Laurent Charpentier <laurent_pubs@yahoo.com>
   Copyright (C) 2007 Luke <iceyfor@gmail.com>
   Copyright (C) 2010 Chris Berry <s0457957@sms.ed.ac.uk> and Michael Rickmann <mrickma@gwdg.de>
                 for Plustek Opticbook 3600 support

   Dynamic rasterization code was taken from the epjistsu backend by
   m. allan noah <kitno455 at gmail dot com>

   Software processing for deskew, crop and dspeckle are inspired by allan's
   noah work in the fujitsu backend

   This file is part of the SANE package.

   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 program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

/*
 * SANE backend for Genesys Logic GL646/GL841/GL842/GL843/GL846/GL847/GL124 based scanners
 */

#define DEBUG_NOT_STATIC

#include "genesys.h"
#include "gl124_registers.h"
#include "gl841_registers.h"
#include "gl842_registers.h"
#include "gl843_registers.h"
#include "gl846_registers.h"
#include "gl847_registers.h"
#include "usb_device.h"
#include "utilities.h"
#include "scanner_interface_usb.h"
#include "test_scanner_interface.h"
#include "test_settings.h"
#include "../include/sane/sanei_config.h"

#include <array>
#include <cmath>
#include <cstring>
#include <fstream>
#include <iterator>
#include <list>
#include <numeric>
#include <exception>
#include <vector>

#ifndef SANE_GENESYS_API_LINKAGE
#define SANE_GENESYS_API_LINKAGE extern "C"
#endif

namespace genesys {

// Data that we allocate to back SANE_Device objects in s_sane_devices
struct SANE_Device_Data
{
    std::string name;
};

namespace {
    StaticInit<std::list<Genesys_Scanner>> s_scanners;
    StaticInit<std::vector<SANE_Device>> s_sane_devices;
    StaticInit<std::vector<SANE_Device_Data>> s_sane_devices_data;
    StaticInit<std::vector<SANE_Device*>> s_sane_devices_ptrs;
    StaticInit<std::list<Genesys_Device>> s_devices;

    // Maximum time for lamp warm-up
    constexpr unsigned WARMUP_TIME = 65;
} // namespace

static SANE_String_Const mode_list[] = {
  SANE_VALUE_SCAN_MODE_COLOR,
  SANE_VALUE_SCAN_MODE_GRAY,
    // SANE_TITLE_HALFTONE, not used
    // SANE_VALUE_SCAN_MODE_LINEART, not used
    nullptr
};

static SANE_String_Const color_filter_list[] = {
  SANE_I18N ("Red"),
  SANE_I18N ("Green"),
  SANE_I18N ("Blue"),
    nullptr
};

static SANE_String_Const cis_color_filter_list[] = {
  SANE_I18N ("Red"),
  SANE_I18N ("Green"),
  SANE_I18N ("Blue"),
  SANE_I18N ("None"),
    nullptr
};

static SANE_Range time_range = {
  0,				/* minimum */
  60,				/* maximum */
  0				/* quantization */
};

static const SANE_Range u12_range = {
  0,				/* minimum */
  4095,				/* maximum */
  0				/* quantization */
};

static const SANE_Range u14_range = {
  0,				/* minimum */
  16383,			/* maximum */
  0				/* quantization */
};

static const SANE_Range u16_range = {
  0,				/* minimum */
  65535,			/* maximum */
  0				/* quantization */
};

static const SANE_Range percentage_range = {
    float_to_fixed(0),     // minimum
    float_to_fixed(100),   // maximum
    float_to_fixed(1)      // quantization
};

/**
 * range for brightness and contrast
 */
static const SANE_Range enhance_range = {
  -100,	/* minimum */
  100,		/* maximum */
  1		/* quantization */
};

/**
 * range for expiration time
 */
static const SANE_Range expiration_range = {
  -1,	        /* minimum */
  30000,	/* maximum */
  1		/* quantization */
};

const Genesys_Sensor& sanei_genesys_find_sensor_any(const Genesys_Device* dev)
{
    DBG_HELPER(dbg);
    for (const auto& sensor : *s_sensors) {
        if (dev->model->sensor_id == sensor.sensor_id) {
            return sensor;
        }
    }
    throw std::runtime_error("Given device does not have sensor defined");
}

Genesys_Sensor* find_sensor_impl(const Genesys_Device* dev, unsigned dpi, unsigned channels,
                                 ScanMethod scan_method)
{
    DBG_HELPER_ARGS(dbg, "dpi: %d, channels: %d, scan_method: %d", dpi, channels,
                    static_cast<unsigned>(scan_method));
    for (auto& sensor : *s_sensors) {
        if (dev->model->sensor_id == sensor.sensor_id && sensor.resolutions.matches(dpi) &&
            sensor.matches_channel_count(channels) && sensor.method == scan_method)
        {
            return &sensor;
        }
    }
    return nullptr;
}

bool sanei_genesys_has_sensor(const Genesys_Device* dev, unsigned dpi, unsigned channels,
                              ScanMethod scan_method)
{
    DBG_HELPER_ARGS(dbg, "dpi: %d, channels: %d, scan_method: %d", dpi, channels,
                    static_cast<unsigned>(scan_method));
    return find_sensor_impl(dev, dpi, channels, scan_method) != nullptr;
}

const Genesys_Sensor& sanei_genesys_find_sensor(const Genesys_Device* dev, unsigned dpi,
                                                unsigned channels, ScanMethod scan_method)
{
    DBG_HELPER_ARGS(dbg, "dpi: %d, channels: %d, scan_method: %d", dpi, channels,
                    static_cast<unsigned>(scan_method));
    const auto* sensor = find_sensor_impl(dev, dpi, channels, scan_method);
    if (sensor)
        return *sensor;
    throw std::runtime_error("Given device does not have sensor defined");
}

Genesys_Sensor& sanei_genesys_find_sensor_for_write(Genesys_Device* dev, unsigned dpi,
                                                    unsigned channels,
                                                    ScanMethod scan_method)
{
    DBG_HELPER_ARGS(dbg, "dpi: %d, channels: %d, scan_method: %d", dpi, channels,
                    static_cast<unsigned>(scan_method));
    auto* sensor = find_sensor_impl(dev, dpi, channels, scan_method);
    if (sensor)
        return *sensor;
    throw std::runtime_error("Given device does not have sensor defined");
}


std::vector<std::reference_wrapper<const Genesys_Sensor>>
    sanei_genesys_find_sensors_all(const Genesys_Device* dev, ScanMethod scan_method)
{
    DBG_HELPER_ARGS(dbg, "scan_method: %d", static_cast<unsigned>(scan_method));
    std::vector<std::reference_wrapper<const Genesys_Sensor>> ret;
    for (auto& sensor : *s_sensors) {
        if (dev->model->sensor_id == sensor.sensor_id && sensor.method == scan_method) {
            ret.push_back(sensor);
        }
    }
    return ret;
}

std::vector<std::reference_wrapper<Genesys_Sensor>>
    sanei_genesys_find_sensors_all_for_write(Genesys_Device* dev, ScanMethod scan_method)
{
    DBG_HELPER_ARGS(dbg, "scan_method: %d", static_cast<unsigned>(scan_method));
    std::vector<std::reference_wrapper<Genesys_Sensor>> ret;
    for (auto& sensor : *s_sensors) {
        if (dev->model->sensor_id == sensor.sensor_id && sensor.method == scan_method) {
            ret.push_back(sensor);
        }
    }
    return ret;
}

void sanei_genesys_init_structs (Genesys_Device * dev)
{
    DBG_HELPER(dbg);

    bool gpo_ok = false;
    bool motor_ok = false;
    bool fe_ok = false;

  /* initialize the GPO data stuff */
    for (const auto& gpo : *s_gpo) {
        if (dev->model->gpio_id == gpo.id) {
            dev->gpo = gpo;
            gpo_ok = true;
            break;
        }
    }

    // initialize the motor data stuff
    for (const auto& motor : *s_motors) {
        if (dev->model->motor_id == motor.id) {
            dev->motor = motor;
            motor_ok = true;
            break;
        }
    }

    for (const auto& frontend : *s_frontends) {
        if (dev->model->adc_id == frontend.id) {
            dev->frontend_initial = frontend;
            dev->frontend = frontend;
            fe_ok = true;
            break;
        }
    }

    if (dev->model->asic_type == AsicType::GL845 ||
        dev->model->asic_type == AsicType::GL846 ||
        dev->model->asic_type == AsicType::GL847 ||
        dev->model->asic_type == AsicType::GL124)
    {
        bool memory_layout_found = false;
        for (const auto& memory_layout : *s_memory_layout) {
            if (memory_layout.models.matches(dev->model->model_id)) {
                dev->memory_layout = memory_layout;
                memory_layout_found = true;
                break;
            }
        }
        if (!memory_layout_found) {
            throw SaneException("Could not find memory layout");
        }
    }

    if (!motor_ok || !gpo_ok || !fe_ok) {
        throw SaneException("bad description(s) for fe/gpo/motor=%d/%d/%d\n",
                            static_cast<unsigned>(dev->model->sensor_id),
                            static_cast<unsigned>(dev->model->gpio_id),
                            static_cast<unsigned>(dev->model->motor_id));
    }
}

/** @brief computes gamma table
 * Generates a gamma table of the given length within 0 and the given
 * maximum value
 * @param gamma_table gamma table to fill
 * @param size size of the table
 * @param maximum value allowed for gamma
 * @param gamma_max maximum gamma value
 * @param gamma gamma to compute values
 * @return a gamma table filled with the computed values
 * */
void
sanei_genesys_create_gamma_table (std::vector<uint16_t>& gamma_table, int size,
                                  float maximum, float gamma_max, float gamma)
{
    gamma_table.clear();
    gamma_table.resize(size, 0);

  int i;
  float value;

  DBG(DBG_proc, "%s: size = %d, ""maximum = %g, gamma_max = %g, gamma = %g\n", __func__, size,
      maximum, gamma_max, gamma);
  for (i = 0; i < size; i++)
    {
        value = static_cast<float>(gamma_max * std::pow(static_cast<double>(i) / size, 1.0 / gamma));
        if (value > maximum) {
            value = maximum;
        }
        gamma_table[i] = static_cast<std::uint16_t>(value);
    }
  DBG(DBG_proc, "%s: completed\n", __func__);
}

void sanei_genesys_create_default_gamma_table(Genesys_Device* dev,
                                              std::vector<uint16_t>& gamma_table, float gamma)
{
    int size = 0;
    int max = 0;
    if (dev->model->asic_type == AsicType::GL646) {
        if (has_flag(dev->model->flags, ModelFlag::GAMMA_14BIT)) {
            size = 16384;
        } else {
            size = 4096;
        }
        max = size - 1;
    } else if (dev->model->asic_type == AsicType::GL124 ||
               dev->model->asic_type == AsicType::GL846 ||
               dev->model->asic_type == AsicType::GL847) {
        size = 257;
        max = 65535;
    } else {
        size = 256;
        max = 65535;
    }
    sanei_genesys_create_gamma_table(gamma_table, size, max, max, gamma);
}

/* computes the exposure_time on the basis of the given vertical dpi,
   the number of pixels the ccd needs to send,
   the step_type and the corresponding maximum speed from the motor struct */
/*
  Currently considers maximum motor speed at given step_type, minimum
  line exposure needed for conversion and led exposure time.

  TODO: Should also consider maximum transfer rate: ~6.5MB/s.
    Note: The enhance option of the scanners does _not_ help. It only halves
          the amount of pixels transferred.
 */
SANE_Int sanei_genesys_exposure_time2(Genesys_Device * dev, const MotorProfile& profile, float ydpi,
                                      int endpixel, int exposure_by_led)
{
  int exposure_by_ccd = endpixel + 32;
    unsigned max_speed_motor_w = profile.slope.max_speed_w;
    int exposure_by_motor = static_cast<int>((max_speed_motor_w * dev->motor.base_ydpi) / ydpi);

  int exposure = exposure_by_ccd;

    if (exposure < exposure_by_motor) {
        exposure = exposure_by_motor;
    }

    if (exposure < exposure_by_led && dev->model->is_cis) {
        exposure = exposure_by_led;
    }

    return exposure;
}


/* Sends a block of shading information to the scanner.
   The data is placed at address 0x0000 for color mode, gray mode and
   unconditionally for the following CCD chips: HP2300, HP2400 and HP5345

   The data needs to be of size "size", and in little endian byte order.
 */
static void genesys_send_offset_and_shading(Genesys_Device* dev, const Genesys_Sensor& sensor,
                                            uint8_t* data, int size)
{
    DBG_HELPER_ARGS(dbg, "(size = %d)", size);
  int start_address;

  /* ASIC higher than gl843 doesn't have register 2A/2B, so we route to
   * a per ASIC shading data loading function if available.
   * It is also used for scanners using SHDAREA */
    if (dev->cmd_set->has_send_shading_data()) {
        dev->cmd_set->send_shading_data(dev, sensor, data, size);
        return;
    }

    start_address = 0x00;

    dev->interface->write_buffer(0x3c, start_address, data, size);
}

void sanei_genesys_init_shading_data(Genesys_Device* dev, const Genesys_Sensor& sensor,
                                     int pixels_per_line)
{
    DBG_HELPER_ARGS(dbg, "pixels_per_line: %d", pixels_per_line);

    if (dev->cmd_set->has_send_shading_data()) {
        return;
    }

  DBG(DBG_proc, "%s (pixels_per_line = %d)\n", __func__, pixels_per_line);

    unsigned channels = dev->settings.get_channels();

  // 16 bit black, 16 bit white
  std::vector<uint8_t> shading_data(pixels_per_line * 4 * channels, 0);

  uint8_t* shading_data_ptr = shading_data.data();

    for (unsigned i = 0; i < pixels_per_line * channels; i++) {
      *shading_data_ptr++ = 0x00;	/* dark lo */
      *shading_data_ptr++ = 0x00;	/* dark hi */
      *shading_data_ptr++ = 0x00;	/* white lo */
      *shading_data_ptr++ = 0x40;	/* white hi -> 0x4000 */
    }

    genesys_send_offset_and_shading(dev, sensor, shading_data.data(),
                                    pixels_per_line * 4 * channels);
}

namespace gl124 {
    void gl124_setup_scan_gpio(Genesys_Device* dev, int resolution);
} // namespace gl124

void scanner_clear_scan_and_feed_counts(Genesys_Device& dev)
{
    switch (dev.model->asic_type) {
        case AsicType::GL841: {
            dev.interface->write_register(gl841::REG_0x0D,
                                          gl841::REG_0x0D_CLRLNCNT);
            break;
        }
        case AsicType::GL842: {
            dev.interface->write_register(gl842::REG_0x0D,
                                          gl842::REG_0x0D_CLRLNCNT);
            break;
        }
        case AsicType::GL843: {
            dev.interface->write_register(gl843::REG_0x0D,
                                          gl843::REG_0x0D_CLRLNCNT | gl843::REG_0x0D_CLRMCNT);
            break;
        }
        case AsicType::GL845:
        case AsicType::GL846: {
            dev.interface->write_register(gl846::REG_0x0D,
                                          gl846::REG_0x0D_CLRLNCNT | gl846::REG_0x0D_CLRMCNT);
            break;
        }
        case AsicType::GL847:{
            dev.interface->write_register(gl847::REG_0x0D,
                                          gl847::REG_0x0D_CLRLNCNT | gl847::REG_0x0D_CLRMCNT);
            break;
        }
        case AsicType::GL124:{
            dev.interface->write_register(gl124::REG_0x0D,
                                          gl124::REG_0x0D_CLRLNCNT | gl124::REG_0x0D_CLRMCNT);
            break;
        }
        default:
            throw SaneException("Unsupported asic type");
    }
}

void scanner_send_slope_table(Genesys_Device* dev, const Genesys_Sensor& sensor, unsigned table_nr,
                              const std::vector<uint16_t>& slope_table)
{
    DBG_HELPER_ARGS(dbg, "table_nr = %d, steps = %zu", table_nr, slope_table.size());

    unsigned max_table_nr = 0;
    switch (dev->model->asic_type) {
        case AsicType::GL646: {
            max_table_nr = 2;
            break;
        }
        case AsicType::GL841:
        case AsicType::GL842:
        case AsicType::GL843:
        case AsicType::GL845:
        case AsicType::GL846:
        case AsicType::GL847:
        case AsicType::GL124: {
            max_table_nr = 4;
            break;
        }
        default:
            throw SaneException("Unsupported ASIC type");
    }

    if (table_nr > max_table_nr) {
        throw SaneException("invalid table number %d", table_nr);
    }

    std::vector<uint8_t> table;
    table.reserve(slope_table.size() * 2);
    for (std::size_t i = 0; i < slope_table.size(); i++) {
        table.push_back(slope_table[i] & 0xff);
        table.push_back(slope_table[i] >> 8);
    }
    if (dev->model->asic_type == AsicType::GL841 ||
        dev->model->model_id == ModelId::CANON_LIDE_90)
    {
        // BUG: do this on all gl842 scanners
        auto max_table_size = get_slope_table_max_size(dev->model->asic_type);
        table.reserve(max_table_size * 2);
        while (table.size() < max_table_size * 2) {
            table.push_back(slope_table.back() & 0xff);
            table.push_back(slope_table.back() >> 8);
        }
    }

    if (dev->interface->is_mock()) {
        dev->interface->record_slope_table(table_nr, slope_table);
    }

    switch (dev->model->asic_type) {
        case AsicType::GL646: {
            unsigned dpihw = dev->reg.find_reg(0x05).value >> 6;
            unsigned start_address = 0;
            if (dpihw == 0) { // 600 dpi
                start_address = 0x08000;
            } else if (dpihw == 1) { // 1200 dpi
                start_address = 0x10000;
            } else if (dpihw == 2) { // 2400 dpi
                start_address = 0x1f800;
            } else {
                throw SaneException("Unexpected dpihw");
            }
            dev->interface->write_buffer(0x3c, start_address + table_nr * 0x100, table.data(),
                                         table.size());
            break;
        }
        case AsicType::GL841:
        case AsicType::GL842: {
            unsigned start_address = 0;
            switch (sensor.register_dpihw) {
                case 600: start_address = 0x08000; break;
                case 1200: start_address = 0x10000; break;
                case 2400: start_address = 0x20000; break;
                default: throw SaneException("Unexpected dpihw");
            }
            dev->interface->write_buffer(0x3c, start_address + table_nr * 0x200, table.data(),
                                         table.size());
            break;
        }
        case AsicType::GL843: {
            // slope table addresses are fixed : 0x40000,  0x48000,  0x50000,  0x58000,  0x60000
            // XXX STEF XXX USB 1.1 ? sanei_genesys_write_0x8c (dev, 0x0f, 0x14);
            dev->interface->write_gamma(0x28,  0x40000 + 0x8000 * table_nr, table.data(),
                                        table.size());
            break;
        }
        case AsicType::GL845:
        case AsicType::GL846:
        case AsicType::GL847:
        case AsicType::GL124: {
            // slope table addresses are fixed
            dev->interface->write_ahb(0x10000000 + 0x4000 * table_nr, table.size(),
                                      table.data());
            break;
        }
        default:
            throw SaneException("Unsupported ASIC type");
    }

}

bool scanner_is_motor_stopped(Genesys_Device& dev)
{
    switch (dev.model->asic_type) {
        case AsicType::GL646: {
            auto status = scanner_read_status(dev);
            return !status.is_motor_enabled && status.is_feeding_finished;
        }
        case AsicType::GL841: {
            auto status = scanner_read_status(dev);
            auto reg = dev.interface->read_register(gl841::REG_0x40);

            return (!(reg & gl841::REG_0x40_DATAENB) && !(reg & gl841::REG_0x40_MOTMFLG) &&
                    !status.is_motor_enabled);
        }
        case AsicType::GL842: {
            auto status = scanner_read_status(dev);
            auto reg = dev.interface->read_register(gl842::REG_0x40);

            return (!(reg & gl842::REG_0x40_DATAENB) && !(reg & gl842::REG_0x40_MOTMFLG) &&
                    !status.is_motor_enabled);
        }
        case AsicType::GL843: {
            auto status = scanner_read_status(dev);
            auto reg = dev.interface->read_register(gl843::REG_0x40);

            return (!(reg & gl843::REG_0x40_DATAENB) && !(reg & gl843::REG_0x40_MOTMFLG) &&
                    !status.is_motor_enabled);
        }
        case AsicType::GL845:
        case AsicType::GL846: {
            auto status = scanner_read_status(dev);
            auto reg = dev.interface->read_register(gl846::REG_0x40);

            return (!(reg & gl846::REG_0x40_DATAENB) && !(reg & gl846::REG_0x40_MOTMFLG) &&
                    !status.is_motor_enabled);
        }
        case AsicType::GL847: {
            auto status = scanner_read_status(dev);
            auto reg = dev.interface->read_register(gl847::REG_0x40);

            return (!(reg & gl847::REG_0x40_DATAENB) && !(reg & gl847::REG_0x40_MOTMFLG) &&
                    !status.is_motor_enabled);
        }
        case AsicType::GL124: {
            auto status = scanner_read_status(dev);
            auto reg = dev.interface->read_register(gl124::REG_0x100);

            return (!(reg & gl124::REG_0x100_DATAENB) && !(reg & gl124::REG_0x100_MOTMFLG) &&
                    !status.is_motor_enabled);
        }
        default:
            throw SaneException("Unsupported asic type");
    }
}

void scanner_setup_sensor(Genesys_Device& dev, const Genesys_Sensor& sensor,
                          Genesys_Register_Set& regs)
{
    DBG_HELPER(dbg);

    for (const auto& custom_reg : sensor.custom_regs) {
        regs.set8(custom_reg.address, custom_reg.value);
    }

    if (dev.model->asic_type != AsicType::GL843)
    {
        // FIXME: remove the above check
        regs_set_exposure(dev.model->asic_type, regs, sensor.exposure);
    }

    dev.segment_order = sensor.segment_order;
}

void scanner_stop_action(Genesys_Device& dev)
{
    DBG_HELPER(dbg);

    switch (dev.model->asic_type) {
        case AsicType::GL841:
        case AsicType::GL842:
        case AsicType::GL843:
        case AsicType::GL845:
        case AsicType::GL846:
        case AsicType::GL847:
        case AsicType::GL124:
            break;
        default:
            throw SaneException("Unsupported asic type");
    }

    dev.cmd_set->update_home_sensor_gpio(dev);

    if (scanner_is_motor_stopped(dev)) {
        DBG(DBG_info, "%s: already stopped\n", __func__);
        return;
    }

    scanner_stop_action_no_move(dev, dev.reg);

    if (is_testing_mode()) {
        return;
    }

    for (unsigned i = 0; i < 10; ++i) {
        if (scanner_is_motor_stopped(dev)) {
            return;
        }

        dev.interface->sleep_ms(100);
    }

    throw SaneException(SANE_STATUS_IO_ERROR, "could not stop motor");
}

void scanner_stop_action_no_move(Genesys_Device& dev, genesys::Genesys_Register_Set& regs)
{
    switch (dev.model->asic_type) {
        case AsicType::GL646:
        case AsicType::GL841:
        case AsicType::GL842:
        case AsicType::GL843:
        case AsicType::GL845:
        case AsicType::GL846:
        case AsicType::GL847:
        case AsicType::GL124:
            break;
        default:
            throw SaneException("Unsupported asic type");
    }

    regs_set_optical_off(dev.model->asic_type, regs);
    // same across all supported ASICs
    dev.interface->write_register(0x01, regs.get8(0x01));

    // looks like certain scanners lock up if we try to scan immediately after stopping previous
    // action.
    dev.interface->sleep_ms(100);
}

void scanner_move(Genesys_Device& dev, ScanMethod scan_method, unsigned steps, Direction direction)
{
    DBG_HELPER_ARGS(dbg, "steps=%d direction=%d", steps, static_cast<unsigned>(direction));

    auto local_reg = dev.reg;

    unsigned resolution = dev.model->get_resolution_settings(scan_method).get_min_resolution_y();

    const auto& sensor = sanei_genesys_find_sensor(&dev, resolution, 3, scan_method);

    bool uses_secondary_head = (scan_method == ScanMethod::TRANSPARENCY ||
                                scan_method == ScanMethod::TRANSPARENCY_INFRARED) &&
                               (!has_flag(dev.model->flags, ModelFlag::UTA_NO_SECONDARY_MOTOR));

    bool uses_secondary_pos = uses_secondary_head &&
                              dev.model->default_method == ScanMethod::FLATBED;

    if (!dev.is_head_pos_known(ScanHeadId::PRIMARY)) {
        throw SaneException("Unknown head position");
    }
    if (uses_secondary_pos && !dev.is_head_pos_known(ScanHeadId::SECONDARY)) {
        throw SaneException("Unknown head position");
    }
    if (direction == Direction::BACKWARD && steps > dev.head_pos(ScanHeadId::PRIMARY)) {
        throw SaneException("Trying to feed behind the home position %d %d",
                            steps, dev.head_pos(ScanHeadId::PRIMARY));
    }
    if (uses_secondary_pos && direction == Direction::BACKWARD &&
        steps > dev.head_pos(ScanHeadId::SECONDARY))
    {
        throw SaneException("Trying to feed behind the home position %d %d",
                            steps, dev.head_pos(ScanHeadId::SECONDARY));
    }

    ScanSession session;
    session.params.xres = resolution;
    session.params.yres = resolution;
    session.params.startx = 0;
    session.params.starty = steps;
    session.params.pixels = 50;
    session.params.lines = 3;
    session.params.depth = 8;
    session.params.channels = 1;
    session.params.scan_method = scan_method;
    session.params.scan_mode = ScanColorMode::GRAY;
    session.params.color_filter = ColorFilter::GREEN;
    session.params.contrast_adjustment = dev.settings.contrast;
    session.params.brightness_adjustment = dev.settings.brightness;

    session.params.flags = ScanFlag::DISABLE_SHADING |
                           ScanFlag::DISABLE_GAMMA |
                           ScanFlag::FEEDING |
                           ScanFlag::IGNORE_STAGGER_OFFSET |
                           ScanFlag::IGNORE_COLOR_OFFSET;

    if (dev.model->asic_type == AsicType::GL124) {
        session.params.flags |= ScanFlag::DISABLE_BUFFER_FULL_MOVE;
    }

    if (direction == Direction::BACKWARD) {
        session.params.flags |= ScanFlag::REVERSE;
    }

    compute_session(&dev, session, sensor);

    dev.cmd_set->init_regs_for_scan_session(&dev, sensor, &local_reg, session);

    if (dev.model->asic_type != AsicType::GL843) {
        regs_set_exposure(dev.model->asic_type, local_reg,
                          sanei_genesys_fixup_exposure({0, 0, 0}));
    }
    scanner_clear_scan_and_feed_counts(dev);

    dev.interface->write_registers(local_reg);
    if (uses_secondary_head) {
        dev.cmd_set->set_motor_mode(dev, local_reg, MotorMode::PRIMARY_AND_SECONDARY);
    }

    try {
        scanner_start_action(dev, true);
    } catch (...) {
        catch_all_exceptions(__func__, [&]() {
            dev.cmd_set->set_motor_mode(dev, local_reg, MotorMode::PRIMARY);
        });
        catch_all_exceptions(__func__, [&]() { scanner_stop_action(dev); });
        // restore original registers
        catch_all_exceptions(__func__, [&]() { dev.interface->write_registers(dev.reg); });
        throw;
    }

    if (is_testing_mode()) {
        dev.interface->test_checkpoint("feed");

        dev.advance_head_pos_by_steps(ScanHeadId::PRIMARY, direction, steps);
        if (uses_secondary_pos) {
            dev.advance_head_pos_by_steps(ScanHeadId::SECONDARY, direction, steps);
        }

        scanner_stop_action(dev);
        if (uses_secondary_head) {
            dev.cmd_set->set_motor_mode(dev, local_reg, MotorMode::PRIMARY);
        }
        return;
    }

    // wait until feed count reaches the required value
    if (dev.model->model_id == ModelId::CANON_LIDE_700F) {
        dev.cmd_set->update_home_sensor_gpio(dev);
    }

    // FIXME: should porbably wait for some timeout
    Status status;
    for (unsigned i = 0;; ++i) {
        status = scanner_read_status(dev);
        if (status.is_feeding_finished || (
            direction == Direction::BACKWARD && status.is_at_home))
        {
            break;
        }
        dev.interface->sleep_ms(10);
    }

    scanner_stop_action(dev);
    if (uses_secondary_head) {
        dev.cmd_set->set_motor_mode(dev, local_reg, MotorMode::PRIMARY);
    }

    dev.advance_head_pos_by_steps(ScanHeadId::PRIMARY, direction, steps);
    if (uses_secondary_pos) {
        dev.advance_head_pos_by_steps(ScanHeadId::SECONDARY, direction, steps);
    }

    // looks like certain scanners lock up if we scan immediately after feeding
    dev.interface->sleep_ms(100);
}

void scanner_move_to_ta(Genesys_Device& dev)
{
    DBG_HELPER(dbg);

    unsigned feed = static_cast<unsigned>((dev.model->y_offset_sensor_to_ta * dev.motor.base_ydpi) /
                                           MM_PER_INCH);
    scanner_move(dev, dev.model->default_method, feed, Direction::FORWARD);
}

void scanner_move_back_home(Genesys_Device& dev, bool wait_until_home)
{
    DBG_HELPER_ARGS(dbg, "wait_until_home = %d", wait_until_home);

    switch (dev.model->asic_type) {
        case AsicType::GL841:
        case AsicType::GL842:
        case AsicType::GL843:
        case AsicType::GL845:
        case AsicType::GL846:
        case AsicType::GL847:
        case AsicType::GL124:
            break;
        default:
            throw SaneException("Unsupported asic type");
    }

    if (dev.model->is_sheetfed) {
        dbg.vlog(DBG_proc, "sheetfed scanner, skipping going back home");
        return;
    }

    // FIXME: also check whether the scanner actually has a secondary head
    if ((!dev.is_head_pos_known(ScanHeadId::SECONDARY) ||
        dev.head_pos(ScanHeadId::SECONDARY) > 0 ||
        dev.settings.scan_method == ScanMethod::TRANSPARENCY ||
        dev.settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) &&
            (!has_flag(dev.model->flags, ModelFlag::UTA_NO_SECONDARY_MOTOR)))
    {
        scanner_move_back_home_ta(dev);
    }

    if (dev.is_head_pos_known(ScanHeadId::PRIMARY) &&
        dev.head_pos(ScanHeadId::PRIMARY) > 1000)
    {
        // leave 500 steps for regular slow back home
        scanner_move(dev, dev.model->default_method, dev.head_pos(ScanHeadId::PRIMARY) - 500,
                     Direction::BACKWARD);
    }

    dev.cmd_set->update_home_sensor_gpio(dev);

    auto status = scanner_read_reliable_status(dev);

    if (status.is_at_home) {
        dbg.log(DBG_info, "already at home");
        dev.set_head_pos_zero(ScanHeadId::PRIMARY);
        return;
    }

    Genesys_Register_Set local_reg = dev.reg;
    unsigned resolution = sanei_genesys_get_lowest_ydpi(&dev);

    const auto& sensor = sanei_genesys_find_sensor(&dev, resolution, 1, dev.model->default_method);

    ScanSession session;
    session.params.xres = resolution;
    session.params.yres = resolution;
    session.params.startx = 0;
    session.params.starty = 40000;
    session.params.pixels = 50;
    session.params.lines = 3;
    session.params.depth = 8;
    session.params.channels = 1;
    session.params.scan_method = dev.settings.scan_method;
    session.params.scan_mode = ScanColorMode::GRAY;
    session.params.color_filter = ColorFilter::GREEN;
    session.params.contrast_adjustment = dev.settings.contrast;
    session.params.brightness_adjustment = dev.settings.brightness;

    session.params.flags =  ScanFlag::DISABLE_SHADING |
                            ScanFlag::DISABLE_GAMMA |
                            ScanFlag::IGNORE_STAGGER_OFFSET |
                            ScanFlag::IGNORE_COLOR_OFFSET |
                            ScanFlag::REVERSE;

    if (dev.model->asic_type == AsicType::GL843) {
        session.params.flags |= ScanFlag::DISABLE_BUFFER_FULL_MOVE;
    }

    compute_session(&dev, session, sensor);

    dev.cmd_set->init_regs_for_scan_session(&dev, sensor, &local_reg, session);

    scanner_clear_scan_and_feed_counts(dev);

    dev.interface->write_registers(local_reg);

    if (dev.model->asic_type == AsicType::GL124) {
        gl124::gl124_setup_scan_gpio(&dev, resolution);
    }

    try {
        scanner_start_action(dev, true);
    } catch (...) {
        catch_all_exceptions(__func__, [&]() { scanner_stop_action(dev); });
        // restore original registers
        catch_all_exceptions(__func__, [&]()
        {
            dev.interface->write_registers(dev.reg);
        });
        throw;
    }

    dev.cmd_set->update_home_sensor_gpio(dev);

    if (is_testing_mode()) {
        dev.interface->test_checkpoint("move_back_home");
        dev.set_head_pos_zero(ScanHeadId::PRIMARY);
        return;
    }

    if (wait_until_home) {
        for (unsigned i = 0; i < 300; ++i) {
            auto status = scanner_read_status(dev);

            if (status.is_at_home) {
                dbg.log(DBG_info, "reached home position");
                if (dev.model->asic_type == AsicType::GL846 ||
                    dev.model->asic_type == AsicType::GL847)
                {
                    scanner_stop_action(dev);
                }
                dev.set_head_pos_zero(ScanHeadId::PRIMARY);
                return;
            }

            dev.interface->sleep_ms(100);
        }

        // when we come here then the scanner needed too much time for this, so we better stop
        // the motor
        catch_all_exceptions(__func__, [&](){ scanner_stop_action(dev); });
        dev.set_head_pos_unknown(ScanHeadId::PRIMARY | ScanHeadId::SECONDARY);
        throw SaneException(SANE_STATUS_IO_ERROR, "timeout while waiting for scanhead to go home");
    }
    dbg.log(DBG_info, "scanhead is still moving");
}

namespace {
    bool should_use_secondary_motor_mode(Genesys_Device& dev)
    {
        bool should_use = !dev.is_head_pos_known(ScanHeadId::SECONDARY) ||
                          !dev.is_head_pos_known(ScanHeadId::PRIMARY) ||
                          dev.head_pos(ScanHeadId::SECONDARY) > dev.head_pos(ScanHeadId::PRIMARY);
        bool supports = dev.model->model_id == ModelId::CANON_8600F;
        return should_use && supports;
    }

    void handle_motor_position_after_move_back_home_ta(Genesys_Device& dev, MotorMode motor_mode)
    {
        if (motor_mode == MotorMode::SECONDARY) {
            dev.set_head_pos_zero(ScanHeadId::SECONDARY);
            return;
        }

        if (dev.is_head_pos_known(ScanHeadId::PRIMARY)) {
            if (dev.head_pos(ScanHeadId::PRIMARY) > dev.head_pos(ScanHeadId::SECONDARY)) {
                dev.advance_head_pos_by_steps(ScanHeadId::PRIMARY, Direction::BACKWARD,
                                              dev.head_pos(ScanHeadId::SECONDARY));
            } else {
                dev.set_head_pos_zero(ScanHeadId::PRIMARY);
            }
            dev.set_head_pos_zero(ScanHeadId::SECONDARY);
        }
    }
} // namespace

void scanner_move_back_home_ta(Genesys_Device& dev)
{
    DBG_HELPER(dbg);

    switch (dev.model->asic_type) {
        case AsicType::GL842:
        case AsicType::GL843:
        case AsicType::GL845:
            break;
        default:
            throw SaneException("Unsupported asic type");
    }

    Genesys_Register_Set local_reg = dev.reg;

    auto scan_method = ScanMethod::TRANSPARENCY;
    unsigned resolution = dev.model->get_resolution_settings(scan_method).get_min_resolution_y();

    const auto& sensor = sanei_genesys_find_sensor(&dev, resolution, 1, scan_method);

    if (dev.is_head_pos_known(ScanHeadId::SECONDARY) &&
        dev.is_head_pos_known(ScanHeadId::PRIMARY) &&
        dev.head_pos(ScanHeadId::SECONDARY) > 1000 &&
        dev.head_pos(ScanHeadId::SECONDARY) <= dev.head_pos(ScanHeadId::PRIMARY))
    {
        // leave 500 steps for regular slow back home
        scanner_move(dev, scan_method, dev.head_pos(ScanHeadId::SECONDARY) - 500,
                     Direction::BACKWARD);
    }

    ScanSession session;
    session.params.xres = resolution;
    session.params.yres = resolution;
    session.params.startx = 0;
    session.params.starty = 40000;
    session.params.pixels = 50;
    session.params.lines = 3;
    session.params.depth = 8;
    session.params.channels = 1;
    session.params.scan_method = scan_method;
    session.params.scan_mode = ScanColorMode::GRAY;
    session.params.color_filter = ColorFilter::GREEN;
    session.params.contrast_adjustment = dev.settings.contrast;
    session.params.brightness_adjustment = dev.settings.brightness;

    session.params.flags =  ScanFlag::DISABLE_SHADING |
                            ScanFlag::DISABLE_GAMMA |
                            ScanFlag::IGNORE_STAGGER_OFFSET |
                            ScanFlag::IGNORE_COLOR_OFFSET |
                            ScanFlag::REVERSE;

    compute_session(&dev, session, sensor);

    dev.cmd_set->init_regs_for_scan_session(&dev, sensor, &local_reg, session);

    scanner_clear_scan_and_feed_counts(dev);

    dev.interface->write_registers(local_reg);

    auto motor_mode = should_use_secondary_motor_mode(dev) ? MotorMode::SECONDARY
                                                           : MotorMode::PRIMARY_AND_SECONDARY;

    dev.cmd_set->set_motor_mode(dev, local_reg, motor_mode);

    try {
        scanner_start_action(dev, true);
    } catch (...) {
        catch_all_exceptions(__func__, [&]() { scanner_stop_action(dev); });
        // restore original registers
        catch_all_exceptions(__func__, [&]() { dev.interface->write_registers(dev.reg); });
        throw;
    }

    if (is_testing_mode()) {
        dev.interface->test_checkpoint("move_back_home_ta");

        handle_motor_position_after_move_back_home_ta(dev, motor_mode);

        scanner_stop_action(dev);
        dev.cmd_set->set_motor_mode(dev, local_reg, MotorMode::PRIMARY);
        return;
    }

    for (unsigned i = 0; i < 1200; ++i) {

        auto status = scanner_read_status(dev);

        if (status.is_at_home) {
            dbg.log(DBG_info, "TA reached home position");

            handle_motor_position_after_move_back_home_ta(dev, motor_mode);

            scanner_stop_action(dev);
            dev.cmd_set->set_motor_mode(dev, local_reg, MotorMode::PRIMARY);
            return;
        }

        dev.interface->sleep_ms(100);
    }

    throw SaneException("Timeout waiting for XPA lamp to park");
}

void scanner_search_strip(Genesys_Device& dev, bool forward, bool black)
{
    DBG_HELPER_ARGS(dbg, "%s %s", black ? "black" : "white", forward ? "forward" : "reverse");

    if (dev.model->asic_type == AsicType::GL841 && !black && forward) {
        dev.frontend.set_gain(0, 0xff);
        dev.frontend.set_gain(1, 0xff);
        dev.frontend.set_gain(2, 0xff);
    }

    // set up for a gray scan at lowest dpi
    const auto& resolution_settings = dev.model->get_resolution_settings(dev.settings.scan_method);
    unsigned dpi = resolution_settings.get_min_resolution_x();
    unsigned channels = 1;

    auto& sensor = sanei_genesys_find_sensor(&dev, dpi, channels, dev.settings.scan_method);
    dev.cmd_set->set_fe(&dev, sensor, AFE_SET);
    scanner_stop_action(dev);


    // shading calibration is done with dev.motor.base_ydpi
    unsigned lines = static_cast<unsigned>(dev.model->y_size_calib_mm * dpi / MM_PER_INCH);
    if (dev.model->asic_type == AsicType::GL841) {
        lines = 10; // TODO: use dev.model->search_lines
        lines = static_cast<unsigned>((lines * dpi) / MM_PER_INCH);
    }

    unsigned pixels = dev.model->x_size_calib_mm * dpi / MM_PER_INCH;

    dev.set_head_pos_zero(ScanHeadId::PRIMARY);

    unsigned length = 20;
    if (dev.model->asic_type == AsicType::GL841) {
        // 20 cm max length for calibration sheet
        length = static_cast<unsigned>(((200 * dpi) / MM_PER_INCH) / lines);
    }

    auto local_reg = dev.reg;

    ScanSession session;
    session.params.xres = dpi;
    session.params.yres = dpi;
    session.params.startx = 0;
    session.params.starty = 0;
    session.params.pixels = pixels;
    session.params.lines = lines;
    session.params.depth = 8;
    session.params.channels = channels;
    session.params.scan_method = dev.settings.scan_method;
    session.params.scan_mode = ScanColorMode::GRAY;
    session.params.color_filter = ColorFilter::RED;
    session.params.contrast_adjustment = dev.settings.contrast;
    session.params.brightness_adjustment = dev.settings.brightness;
    session.params.flags = ScanFlag::DISABLE_SHADING |
                           ScanFlag::DISABLE_GAMMA;
    if (dev.model->asic_type != AsicType::GL841 && !forward) {
        session.params.flags |= ScanFlag::REVERSE;
    }
    compute_session(&dev, session, sensor);

    dev.cmd_set->init_regs_for_scan_session(&dev, sensor, &local_reg, session);

    dev.interface->write_registers(local_reg);

    dev.cmd_set->begin_scan(&dev, sensor, &local_reg, true);

    if (is_testing_mode()) {
        dev.interface->test_checkpoint("search_strip");
        scanner_stop_action(dev);
        return;
    }

    wait_until_buffer_non_empty(&dev);

    // now we're on target, we can read data
    auto image = read_unshuffled_image_from_scanner(&dev, session, session.output_total_bytes);

    scanner_stop_action(dev);

    unsigned pass = 0;
    if (dbg_log_image_data()) {
        char title[80];
        std::sprintf(title, "gl_search_strip_%s_%s%02d.tiff",
                     black ? "black" : "white", forward ? "fwd" : "bwd", pass);
        write_tiff_file(title, image);
    }

    // loop until strip is found or maximum pass number done
    bool found = false;
    while (pass < length && !found) {
        dev.interface->write_registers(local_reg);

        // now start scan
        dev.cmd_set->begin_scan(&dev, sensor, &local_reg, true);

        wait_until_buffer_non_empty(&dev);

        // now we're on target, we can read data
        image = read_unshuffled_image_from_scanner(&dev, session, session.output_total_bytes);

        scanner_stop_action(dev);

        if (dbg_log_image_data()) {
            char title[80];
            std::sprintf(title, "gl_search_strip_%s_%s%02d.tiff",
                         black ? "black" : "white",
                         forward ? "fwd" : "bwd", static_cast<int>(pass));
            write_tiff_file(title, image);
        }

        unsigned white_level = 90;
        unsigned black_level = 60;

        std::size_t count = 0;
        // Search data to find black strip
        // When searching forward, we only need one line of the searched color since we
        // will scan forward. But when doing backward search, we need all the area of the ame color
        if (forward) {

            for (std::size_t y = 0; y < image.get_height() && !found; y++) {
                count = 0;

                // count of white/black pixels depending on the color searched
                for (std::size_t x = 0; x < image.get_width(); x++) {

                    // when searching for black, detect white pixels
                    if (black && image.get_raw_channel(x, y, 0) > white_level) {
                        count++;
                    }

                    // when searching for white, detect black pixels
                    if (!black && image.get_raw_channel(x, y, 0) < black_level) {
                        count++;
                    }
                }

                // at end of line, if count >= 3%, line is not fully of the desired color
                // so we must go to next line of the buffer */
                // count*100/pixels < 3

                auto found_percentage = (count * 100 / image.get_width());
                if (found_percentage < 3) {
                    found = 1;
                    DBG(DBG_data, "%s: strip found forward during pass %d at line %zu\n", __func__,
                        pass, y);
                } else {
                    DBG(DBG_data, "%s: pixels=%zu, count=%zu (%zu%%)\n", __func__,
                        image.get_width(), count, found_percentage);
                }
            }
        } else {
            /*  since calibration scans are done forward, we need the whole area
                to be of the required color when searching backward
            */
            count = 0;
            for (std::size_t y = 0; y < image.get_height(); y++) {
                // count of white/black pixels depending on the color searched
                for (std::size_t x = 0; x < image.get_width(); x++) {
                    // when searching for black, detect white pixels
                    if (black && image.get_raw_channel(x, y, 0) > white_level) {
                        count++;
                    }
                    // when searching for white, detect black pixels
                    if (!black && image.get_raw_channel(x, y, 0) < black_level) {
                        count++;
                    }
                }
            }

            // at end of area, if count >= 3%, area is not fully of the desired color
            // so we must go to next buffer
            auto found_percentage = count * 100 / (image.get_width() * image.get_height());
            if (found_percentage < 3) {
                found = 1;
                DBG(DBG_data, "%s: strip found backward during pass %d \n", __func__, pass);
            } else {
                DBG(DBG_data, "%s: pixels=%zu, count=%zu (%zu%%)\n", __func__, image.get_width(),
                    count, found_percentage);
            }
        }
        pass++;
    }

    if (found) {
        DBG(DBG_info, "%s: %s strip found\n", __func__, black ? "black" : "white");
    } else {
        throw SaneException(SANE_STATUS_UNSUPPORTED, "%s strip not found",
                            black ? "black" : "white");
    }
}

static int dark_average_channel(const Image& image, unsigned black, unsigned channel)
{
    auto channels = get_pixel_channels(image.get_format());

    unsigned avg[3];

    // computes average values on black margin
    for (unsigned ch = 0; ch < channels; ch++) {
        avg[ch] = 0;
        unsigned count = 0;
        // FIXME: start with the second line because the black pixels often have noise on the first
        // line; the cause is probably incorrectly cleaned up previous scan
        for (std::size_t y = 1; y < image.get_height(); y++) {
            for (unsigned j = 0; j < black; j++) {
                avg[ch] += image.get_raw_channel(j, y, ch);
                count++;
            }
        }
        if (count > 0) {
            avg[ch] /= count;
        }
        DBG(DBG_info, "%s: avg[%d] = %d\n", __func__, ch, avg[ch]);
    }
    DBG(DBG_info, "%s: average = %d\n", __func__, avg[channel]);
    return avg[channel];
}

bool should_calibrate_only_active_area(const Genesys_Device& dev,
                                       const Genesys_Settings& settings)
{
    if (settings.scan_method == ScanMethod::TRANSPARENCY ||
        settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED)
    {
        if (dev.model->model_id == ModelId::CANON_4400F && settings.xres >= 4800) {
            return true;
        }
        if (dev.model->model_id == ModelId::CANON_8600F && settings.xres == 4800) {
            return true;
        }
    }
    return false;
}

void scanner_offset_calibration(Genesys_Device& dev, const Genesys_Sensor& sensor,
                                Genesys_Register_Set& regs)
{
    DBG_HELPER(dbg);

    if (dev.model->asic_type == AsicType::GL842 &&
        dev.frontend.layout.type != FrontendType::WOLFSON)
    {
        return;
    }

    if (dev.model->asic_type == AsicType::GL843 &&
        dev.frontend.layout.type != FrontendType::WOLFSON)
    {
        return;
    }

    if (dev.model->asic_type == AsicType::GL845 ||
        dev.model->asic_type == AsicType::GL846)
    {
        // no gain nor offset for AKM AFE
        std::uint8_t reg04 = dev.interface->read_register(gl846::REG_0x04);
        if ((reg04 & gl846::REG_0x04_FESET) == 0x02) {
            return;
        }
    }
    if (dev.model->asic_type == AsicType::GL847) {
        // no gain nor offset for AKM AFE
        std::uint8_t reg04 = dev.interface->read_register(gl847::REG_0x04);
        if ((reg04 & gl847::REG_0x04_FESET) == 0x02) {
            return;
        }
    }

    if (dev.model->asic_type == AsicType::GL124) {
        std::uint8_t reg0a = dev.interface->read_register(gl124::REG_0x0A);
        if (((reg0a & gl124::REG_0x0A_SIFSEL) >> gl124::REG_0x0AS_SIFSEL) == 3) {
            return;
        }
    }

    unsigned target_pixels = dev.model->x_size_calib_mm * sensor.full_resolution / MM_PER_INCH;
    unsigned start_pixel = 0;
    unsigned black_pixels = (sensor.black_pixels * sensor.full_resolution) / sensor.full_resolution;

    unsigned channels = 3;
    unsigned lines = 1;
    unsigned resolution = sensor.full_resolution;

    const Genesys_Sensor* calib_sensor = &sensor;
    if (dev.model->asic_type == AsicType::GL843) {
        lines = 8;

        // compute divider factor to compute final pixels number
        const auto& dpihw_sensor = sanei_genesys_find_sensor(&dev, dev.settings.xres, channels,
                                                             dev.settings.scan_method);
        resolution = dpihw_sensor.shading_resolution;
        unsigned factor = sensor.full_resolution / resolution;

        calib_sensor = &sanei_genesys_find_sensor(&dev, resolution, channels,
                                                  dev.settings.scan_method);

        target_pixels = dev.model->x_size_calib_mm * resolution / MM_PER_INCH;
        black_pixels = calib_sensor->black_pixels / factor;

        if (should_calibrate_only_active_area(dev, dev.settings)) {
            float offset = dev.model->x_offset_ta;
            start_pixel = static_cast<int>((offset * calib_sensor->get_optical_resolution()) / MM_PER_INCH);

            float size = dev.model->x_size_ta;
            target_pixels = static_cast<int>((size * calib_sensor->get_optical_resolution()) / MM_PER_INCH);
        }

        if (dev.model->model_id == ModelId::CANON_4400F &&
            dev.settings.scan_method == ScanMethod::FLATBED)
        {
            return;
        }
    }

    if (dev.model->model_id == ModelId::CANON_5600F) {
        // FIXME: use same approach as for GL843 scanners
        lines = 8;
    }

    if (dev.model->asic_type == AsicType::GL847) {
        calib_sensor = &sanei_genesys_find_sensor(&dev, resolution, channels,
                                                  dev.settings.scan_method);
    }

    ScanFlag flags = ScanFlag::DISABLE_SHADING |
                     ScanFlag::DISABLE_GAMMA |
                     ScanFlag::SINGLE_LINE |
                     ScanFlag::IGNORE_STAGGER_OFFSET |
                     ScanFlag::IGNORE_COLOR_OFFSET;

    if (dev.settings.scan_method == ScanMethod::TRANSPARENCY ||
        dev.settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED)
    {
        flags |= ScanFlag::USE_XPA;
    }

    ScanSession session;
    session.params.xres = resolution;
    session.params.yres = resolution;
    session.params.startx = start_pixel;
    session.params.starty = 0;
    session.params.pixels = target_pixels;
    session.params.lines = lines;
    session.params.depth = 8;
    session.params.channels = channels;
    session.params.scan_method = dev.settings.scan_method;
    session.params.scan_mode = ScanColorMode::COLOR_SINGLE_PASS;
    session.params.color_filter = dev.model->asic_type == AsicType::GL843 ? ColorFilter::RED
                                                                          : dev.settings.color_filter;
    session.params.contrast_adjustment = dev.settings.contrast;
    session.params.brightness_adjustment = dev.settings.brightness;
    session.params.flags = flags;
    compute_session(&dev, session, *calib_sensor);

    dev.cmd_set->init_regs_for_scan_session(&dev, *calib_sensor, &regs, session);

    unsigned output_pixels = session.output_pixels;

    sanei_genesys_set_motor_power(regs, false);

    int top[3], bottom[3];
    int topavg[3], bottomavg[3], avg[3];

    // init gain and offset
    for (unsigned ch = 0; ch < 3; ch++)
    {
        bottom[ch] = 10;
        dev.frontend.set_offset(ch, bottom[ch]);
        dev.frontend.set_gain(ch, 0);
    }
    dev.cmd_set->set_fe(&dev, *calib_sensor, AFE_SET);

    // scan with bottom AFE settings
    dev.interface->write_registers(regs);
    DBG(DBG_info, "%s: starting first line reading\n", __func__);

    dev.cmd_set->begin_scan(&dev, *calib_sensor, &regs, true);

    if (is_testing_mode()) {
        dev.interface->test_checkpoint("offset_calibration");
        if (dev.model->asic_type == AsicType::GL842 ||
            dev.model->asic_type == AsicType::GL843)
        {
            scanner_stop_action_no_move(dev, regs);
        }
        return;
    }

    Image first_line;
    if (dev.model->asic_type == AsicType::GL842 ||
        dev.model->asic_type == AsicType::GL843)
    {
        first_line = read_unshuffled_image_from_scanner(&dev, session,
                                                        session.output_total_bytes_raw);
        scanner_stop_action_no_move(dev, regs);
    } else {
        first_line = read_unshuffled_image_from_scanner(&dev, session, session.output_total_bytes);

        if (dev.model->model_id == ModelId::CANON_5600F) {
            scanner_stop_action_no_move(dev, regs);
        }
    }

    if (dbg_log_image_data()) {
        char fn[40];
        std::snprintf(fn, 40, "gl843_bottom_offset_%03d_%03d_%03d.tiff",
                      bottom[0], bottom[1], bottom[2]);
        write_tiff_file(fn, first_line);
    }

    for (unsigned ch = 0; ch < 3; ch++) {
        bottomavg[ch] = dark_average_channel(first_line, black_pixels, ch);
        DBG(DBG_info, "%s: bottom avg %d=%d\n", __func__, ch, bottomavg[ch]);
    }

    // now top value
    for (unsigned ch = 0; ch < 3; ch++) {
        top[ch] = 255;
        dev.frontend.set_offset(ch, top[ch]);
    }
    dev.cmd_set->set_fe(&dev, *calib_sensor, AFE_SET);

    // scan with top AFE values
    dev.interface->write_registers(regs);
    DBG(DBG_info, "%s: starting second line reading\n", __func__);

    dev.cmd_set->begin_scan(&dev, *calib_sensor, &regs, true);

    Image second_line;
    if (dev.model->asic_type == AsicType::GL842 ||
        dev.model->asic_type == AsicType::GL843)
    {
        second_line = read_unshuffled_image_from_scanner(&dev, session,
                                                         session.output_total_bytes_raw);
        scanner_stop_action_no_move(dev, regs);
    } else {
        second_line = read_unshuffled_image_from_scanner(&dev, session, session.output_total_bytes);

        if (dev.model->model_id == ModelId::CANON_5600F) {
            scanner_stop_action_no_move(dev, regs);
        }
    }

    for (unsigned ch = 0; ch < 3; ch++){
        topavg[ch] = dark_average_channel(second_line, black_pixels, ch);
        DBG(DBG_info, "%s: top avg %d=%d\n", __func__, ch, topavg[ch]);
    }

    unsigned pass = 0;

    std::vector<std::uint8_t> debug_image;
    std::size_t debug_image_lines = 0;
    std::string debug_image_info;

    // loop until acceptable level
    while ((pass < 32) && ((top[0] - bottom[0] > 1) ||
                           (top[1] - bottom[1] > 1) ||
                           (top[2] - bottom[2] > 1)))
    {
        pass++;

        for (unsigned ch = 0; ch < 3; ch++) {
            if (top[ch] - bottom[ch] > 1) {
                dev.frontend.set_offset(ch, (top[ch] + bottom[ch]) / 2);
            }
        }
        dev.cmd_set->set_fe(&dev, *calib_sensor, AFE_SET);

        // scan with no move
        dev.interface->write_registers(regs);
        DBG(DBG_info, "%s: starting second line reading\n", __func__);
        dev.cmd_set->begin_scan(&dev, *calib_sensor, &regs, true);

        if (dev.model->asic_type == AsicType::GL842 ||
            dev.model->asic_type == AsicType::GL843)
        {
            second_line = read_unshuffled_image_from_scanner(&dev, session,
                                                             session.output_total_bytes_raw);
            scanner_stop_action_no_move(dev, regs);
        } else {
            second_line = read_unshuffled_image_from_scanner(&dev, session, session.output_total_bytes);

            if (dev.model->model_id == ModelId::CANON_5600F) {
                scanner_stop_action_no_move(dev, regs);
            }
        }

        if (dbg_log_image_data()) {
            char title[100];
            std::snprintf(title, 100, "lines: %d pixels_per_line: %d offsets[0..2]: %d %d %d\n",
                          lines, output_pixels,
                          dev.frontend.get_offset(0),
                          dev.frontend.get_offset(1),
                          dev.frontend.get_offset(2));
            debug_image_info += title;
            std::copy(second_line.get_row_ptr(0),
                      second_line.get_row_ptr(0) + second_line.get_row_bytes() * second_line.get_height(),
                      std::back_inserter(debug_image));
            debug_image_lines += lines;
        }

        for (unsigned ch = 0; ch < 3; ch++) {
            avg[ch] = dark_average_channel(second_line, black_pixels, ch);
            DBG(DBG_info, "%s: avg[%d]=%d offset=%d\n", __func__, ch, avg[ch],
                dev.frontend.get_offset(ch));
        }

        // compute new boundaries
        for (unsigned ch = 0; ch < 3; ch++) {
            if (topavg[ch] >= avg[ch]) {
                topavg[ch] = avg[ch];
                top[ch] = dev.frontend.get_offset(ch);
            } else {
                bottomavg[ch] = avg[ch];
                bottom[ch] = dev.frontend.get_offset(ch);
            }
        }
    }

    if (dbg_log_image_data()) {
        sanei_genesys_write_file("gl_offset_all_desc.txt",
                                 reinterpret_cast<const std::uint8_t*>(debug_image_info.data()),
                                 debug_image_info.size());
        write_tiff_file("gl_offset_all.tiff", debug_image.data(), session.params.depth, channels,
                        output_pixels, debug_image_lines);
    }

    DBG(DBG_info, "%s: offset=(%d,%d,%d)\n", __func__,
        dev.frontend.get_offset(0),
        dev.frontend.get_offset(1),
        dev.frontend.get_offset(2));
}

/*  With offset and coarse calibration we only want to get our input range into
    a reasonable shape. the fine calibration of the upper and lower bounds will
    be done with shading.
*/
void scanner_coarse_gain_calibration(Genesys_Device& dev, const Genesys_Sensor& sensor,
                                     Genesys_Register_Set& regs, unsigned dpi)
{
    DBG_HELPER_ARGS(dbg, "dpi = %d", dpi);

    if (dev.model->asic_type == AsicType::GL842 &&
        dev.frontend.layout.type != FrontendType::WOLFSON)
    {
        return;
    }

    if (dev.model->asic_type == AsicType::GL843 &&
        dev.frontend.layout.type != FrontendType::WOLFSON)
    {
        return;
    }

    if (dev.model->asic_type == AsicType::GL845 ||
        dev.model->asic_type == AsicType::GL846)
    {
        // no gain nor offset for AKM AFE
        std::uint8_t reg04 = dev.interface->read_register(gl846::REG_0x04);
        if ((reg04 & gl846::REG_0x04_FESET) == 0x02) {
            return;
        }
    }

    if (dev.model->asic_type == AsicType::GL847) {
        // no gain nor offset for AKM AFE
        std::uint8_t reg04 = dev.interface->read_register(gl847::REG_0x04);
        if ((reg04 & gl847::REG_0x04_FESET) == 0x02) {
            return;
        }
    }

    if (dev.model->asic_type == AsicType::GL124) {
        // no gain nor offset for TI AFE
        std::uint8_t reg0a = dev.interface->read_register(gl124::REG_0x0A);
        if (((reg0a & gl124::REG_0x0A_SIFSEL) >> gl124::REG_0x0AS_SIFSEL) == 3) {
            return;
        }
    }

    if (dev.model->asic_type == AsicType::GL841) {
        // feed to white strip if needed
        if (dev.model->y_offset_calib_white > 0) {
            unsigned move = static_cast<unsigned>(
                    (dev.model->y_offset_calib_white * (dev.motor.base_ydpi)) / MM_PER_INCH);
            scanner_move(dev, dev.model->default_method, move, Direction::FORWARD);
        }
    }

    // coarse gain calibration is always done in color mode
    unsigned channels = 3;

    unsigned resolution = sensor.full_resolution;
    if (dev.model->asic_type == AsicType::GL841) {
        const auto& dpihw_sensor = sanei_genesys_find_sensor(&dev, dev.settings.xres, channels,
                                                             dev.settings.scan_method);
        resolution = dpihw_sensor.shading_resolution;
    }

    if (dev.model->asic_type == AsicType::GL842 ||
        dev.model->asic_type == AsicType::GL843)
    {
        const auto& dpihw_sensor = sanei_genesys_find_sensor(&dev, dpi, channels,
                                                             dev.settings.scan_method);
        resolution = dpihw_sensor.shading_resolution;
    }

    float coeff = 1;

    // Follow CKSEL
    if (dev.model->sensor_id == SensorId::CCD_KVSS080 ||
        dev.model->asic_type == AsicType::GL845 ||
        dev.model->asic_type == AsicType::GL846 ||
        dev.model->asic_type == AsicType::GL847 ||
        dev.model->asic_type == AsicType::GL124)
    {
        if (dev.settings.xres < sensor.full_resolution) {
            coeff = 0.9f;
        }
    }

    unsigned lines = 10;
    if (dev.model->asic_type == AsicType::GL841) {
        lines = 1;
    }

    const Genesys_Sensor* calib_sensor = &sensor;
    if (dev.model->asic_type == AsicType::GL841 ||
        dev.model->asic_type == AsicType::GL842 ||
        dev.model->asic_type == AsicType::GL843 ||
        dev.model->asic_type == AsicType::GL847)
    {
        calib_sensor = &sanei_genesys_find_sensor(&dev, resolution, channels,
                                                  dev.settings.scan_method);
    }

    ScanFlag flags = ScanFlag::DISABLE_SHADING |
                     ScanFlag::DISABLE_GAMMA |
                     ScanFlag::SINGLE_LINE |
                     ScanFlag::IGNORE_STAGGER_OFFSET |
                     ScanFlag::IGNORE_COLOR_OFFSET;

    if (dev.settings.scan_method == ScanMethod::TRANSPARENCY ||
        dev.settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED)
    {
        flags |= ScanFlag::USE_XPA;
    }

    ScanSession session;
    session.params.xres = resolution;
    session.params.yres = dev.model->asic_type == AsicType::GL841 ? dev.settings.yres : resolution;
    session.params.startx = 0;
    session.params.starty = 0;
    session.params.pixels = dev.model->x_size_calib_mm * resolution / MM_PER_INCH;
    session.params.lines = lines;
    session.params.depth = dev.model->asic_type == AsicType::GL841 ? 16 : 8;
    session.params.channels = channels;
    session.params.scan_method = dev.settings.scan_method;
    session.params.scan_mode = ScanColorMode::COLOR_SINGLE_PASS;
    session.params.color_filter = dev.settings.color_filter;
    session.params.contrast_adjustment = dev.settings.contrast;
    session.params.brightness_adjustment = dev.settings.brightness;
    session.params.flags = flags;
    compute_session(&dev, session, *calib_sensor);

    std::size_t pixels = session.output_pixels;

    try {
        dev.cmd_set->init_regs_for_scan_session(&dev, *calib_sensor, &regs, session);
    } catch (...) {
        if (dev.model->asic_type != AsicType::GL841) {
            catch_all_exceptions(__func__, [&](){ sanei_genesys_set_motor_power(regs, false); });
        }
        throw;
    }

    if (dev.model->asic_type != AsicType::GL841) {
        sanei_genesys_set_motor_power(regs, false);
    }

    dev.interface->write_registers(regs);

    if (dev.model->asic_type != AsicType::GL841) {
        dev.cmd_set->set_fe(&dev, *calib_sensor, AFE_SET);
    }
    dev.cmd_set->begin_scan(&dev, *calib_sensor, &regs, true);

    if (is_testing_mode()) {
        dev.interface->test_checkpoint("coarse_gain_calibration");
        scanner_stop_action(dev);
        dev.cmd_set->move_back_home(&dev, true);
        return;
    }

    Image image;
    if (dev.model->asic_type == AsicType::GL842 ||
        dev.model->asic_type == AsicType::GL843)
    {
        image = read_unshuffled_image_from_scanner(&dev, session, session.output_total_bytes_raw);
    } else if (dev.model->asic_type == AsicType::GL124) {
        // BUG: we probably want to read whole image, not just first line
        image = read_unshuffled_image_from_scanner(&dev, session, session.output_line_bytes);
    } else {
        image = read_unshuffled_image_from_scanner(&dev, session, session.output_total_bytes);
    }

    if (dev.model->asic_type == AsicType::GL842 ||
        dev.model->asic_type == AsicType::GL843)
    {
        scanner_stop_action_no_move(dev, regs);
    }

    if (dbg_log_image_data()) {
        write_tiff_file("gl_coarse_gain.tiff", image);
    }

    for (unsigned ch = 0; ch < channels; ch++) {
        float curr_output = 0;
        float target_value = 0;

        if (dev.model->asic_type == AsicType::GL841 ||
            dev.model->asic_type == AsicType::GL842 ||
            dev.model->asic_type == AsicType::GL843)
        {
            std::vector<uint16_t> values;
            // FIXME: start from the second line because the first line often has artifacts. Probably
            // caused by unclean cleanup of previous scan
            for (std::size_t x = pixels / 4; x < (pixels * 3 / 4); x++) {
                values.push_back(image.get_raw_channel(x, 1, ch));
            }

            // pick target value at 95th percentile of all values. There may be a lot of black values
            // in transparency scans for example
            std::sort(values.begin(), values.end());
            curr_output = static_cast<float>(values[unsigned((values.size() - 1) * 0.95)]);
            target_value = calib_sensor->gain_white_ref * coeff;

        } else {
            // FIXME: use the GL843 approach
            auto width = image.get_width();

            std::uint64_t total = 0;
            for (std::size_t x = width / 4; x < (width * 3 / 4); x++) {
                total += image.get_raw_channel(x, 0, ch);
            }

            curr_output = total / (width / 2);
            target_value = calib_sensor->gain_white_ref * coeff;
        }

        std::uint8_t out_gain = compute_frontend_gain(curr_output, target_value,
                                                      dev.frontend.layout.type);
        dev.frontend.set_gain(ch, out_gain);

        DBG(DBG_proc, "%s: channel %d, curr=%f, target=%f, out_gain:%d\n", __func__, ch,
            curr_output, target_value, out_gain);

        if (dev.model->asic_type == AsicType::GL841 &&
            target_value / curr_output > 30)
        {
            DBG(DBG_error0, "****************************************\n");
            DBG(DBG_error0, "*                                      *\n");
            DBG(DBG_error0, "*  Extremely low Brightness detected.  *\n");
            DBG(DBG_error0, "*  Check the scanning head is          *\n");
            DBG(DBG_error0, "*  unlocked and moving.                *\n");
            DBG(DBG_error0, "*                                      *\n");
            DBG(DBG_error0, "****************************************\n");
            throw SaneException(SANE_STATUS_JAMMED, "scanning head is locked");
        }

        dbg.vlog(DBG_info, "gain=(%d, %d, %d)", dev.frontend.get_gain(0), dev.frontend.get_gain(1),
                 dev.frontend.get_gain(2));
    }

    if (dev.model->is_cis) {
        std::uint8_t min_gain = std::min({dev.frontend.get_gain(0),
                                          dev.frontend.get_gain(1),
                                          dev.frontend.get_gain(2)});

        dev.frontend.set_gain(0, min_gain);
        dev.frontend.set_gain(1, min_gain);
        dev.frontend.set_gain(2, min_gain);
    }

    dbg.vlog(DBG_info, "final gain=(%d, %d, %d)", dev.frontend.get_gain(0),
             dev.frontend.get_gain(1), dev.frontend.get_gain(2));

    scanner_stop_action(dev);

    dev.cmd_set->move_back_home(&dev, true);
}

namespace gl124 {
    void move_to_calibration_area(Genesys_Device* dev, const Genesys_Sensor& sensor,
                                  Genesys_Register_Set& regs);
} // namespace gl124

SensorExposure scanner_led_calibration(Genesys_Device& dev, const Genesys_Sensor& sensor,
                                       Genesys_Register_Set& regs)
{
    DBG_HELPER(dbg);

    float move = 0;

    if (dev.model->asic_type == AsicType::GL841) {
        if (dev.model->y_offset_calib_white > 0) {
            move = (dev.model->y_offset_calib_white * (dev.motor.base_ydpi)) / MM_PER_INCH;
            scanner_move(dev, dev.model->default_method, static_cast<unsigned>(move),
                         Direction::FORWARD);
        }
    } else if (dev.model->asic_type == AsicType::GL842 ||
               dev.model->asic_type == AsicType::GL843)
    {
        // do nothing
    } else if (dev.model->asic_type == AsicType::GL845 ||
               dev.model->asic_type == AsicType::GL846 ||
               dev.model->asic_type == AsicType::GL847)
    {
        move = dev.model->y_offset_calib_white;
        move = static_cast<float>((move * (dev.motor.base_ydpi / 4)) / MM_PER_INCH);
        if (move > 20) {
            scanner_move(dev, dev.model->default_method, static_cast<unsigned>(move),
                         Direction::FORWARD);
        }
    } else if (dev.model->asic_type == AsicType::GL124) {
        gl124::move_to_calibration_area(&dev, sensor, regs);
    }


    unsigned channels = 3;
    unsigned resolution = sensor.shading_resolution;
    const auto& calib_sensor = sanei_genesys_find_sensor(&dev, resolution, channels,
                                                         dev.settings.scan_method);

    if (dev.model->asic_type == AsicType::GL841 ||
        dev.model->asic_type == AsicType::GL845 ||
        dev.model->asic_type == AsicType::GL846 ||
        dev.model->asic_type == AsicType::GL847 ||
        dev.model->asic_type == AsicType::GL124)
    {
        regs = dev.reg; // FIXME: apply this to all ASICs
    }

    ScanSession session;
    session.params.xres = resolution;
    session.params.yres = resolution;
    session.params.startx = 0;
    session.params.starty = 0;
    session.params.pixels = dev.model->x_size_calib_mm * resolution / MM_PER_INCH;
    session.params.lines = 1;
    session.params.depth = 16;
    session.params.channels = channels;
    session.params.scan_method = dev.settings.scan_method;
    session.params.scan_mode = ScanColorMode::COLOR_SINGLE_PASS;
    session.params.color_filter = dev.settings.color_filter;
    session.params.contrast_adjustment = dev.settings.contrast;
    session.params.brightness_adjustment = dev.settings.brightness;
    session.params.flags = ScanFlag::DISABLE_SHADING |
                           ScanFlag::DISABLE_GAMMA |
                           ScanFlag::SINGLE_LINE |
                           ScanFlag::IGNORE_STAGGER_OFFSET |
                           ScanFlag::IGNORE_COLOR_OFFSET;
    compute_session(&dev, session, calib_sensor);

    dev.cmd_set->init_regs_for_scan_session(&dev, calib_sensor, &regs, session);

    std::uint16_t exp[3];

    exp[0] = calib_sensor.exposure.red;
    exp[1] = calib_sensor.exposure.green;
    exp[2] = calib_sensor.exposure.blue;

    std::uint16_t target = sensor.gain_white_ref * 256;

    std::uint16_t top[3] = {};
    std::uint16_t bottom[3] = {};

    if (dev.model->asic_type == AsicType::GL845 ||
        dev.model->asic_type == AsicType::GL846)
    {
        bottom[0] = 29000;
        bottom[1] = 29000;
        bottom[2] = 29000;

        top[0] = 41000;
        top[1] = 51000;
        top[2] = 51000;
    } else if (dev.model->asic_type == AsicType::GL847) {
        bottom[0] = 28000;
        bottom[1] = 28000;
        bottom[2] = 28000;

        top[0] = 32000;
        top[1] = 32000;
        top[2] = 32000;
    }

    if (dev.model->asic_type == AsicType::GL845 ||
        dev.model->asic_type == AsicType::GL846 ||
        dev.model->asic_type == AsicType::GL847 ||
        dev.model->asic_type == AsicType::GL124)
    {
        sanei_genesys_set_motor_power(regs, false);
    }

    bool acceptable = false;
    for (unsigned i_test = 0; i_test < 100 && !acceptable; ++i_test) {
        regs_set_exposure(dev.model->asic_type, regs, { exp[0], exp[1], exp[2] });

        dev.interface->write_registers(regs);

        dbg.log(DBG_info, "starting line reading");
        dev.cmd_set->begin_scan(&dev, calib_sensor, &regs, true);

        if (is_testing_mode()) {
            dev.interface->test_checkpoint("led_calibration");
            if (dev.model->asic_type == AsicType::GL841) {
                scanner_stop_action(dev);
                dev.cmd_set->move_back_home(&dev, true);
            } else if (dev.model->asic_type == AsicType::GL124) {
                scanner_stop_action(dev);
            } else {
                scanner_stop_action(dev);
                dev.cmd_set->move_back_home(&dev, true);
            }
            return { exp[0], exp[1], exp[2] };
        }

        auto image = read_unshuffled_image_from_scanner(&dev, session, session.output_line_bytes);

        scanner_stop_action(dev);

        if (dbg_log_image_data()) {
            char fn[30];
            std::snprintf(fn, 30, "gl_led_%02d.tiff", i_test);
            write_tiff_file(fn, image);
        }

        int avg[3];
        for (unsigned ch = 0; ch < channels; ch++) {
            avg[ch] = 0;
            for (std::size_t x = 0; x < image.get_width(); x++) {
                avg[ch] += image.get_raw_channel(x, 0, ch);
            }
            avg[ch] /= image.get_width();
        }

        dbg.vlog(DBG_info, "average: %d, %d, %d", avg[0], avg[1], avg[2]);

        acceptable = true;

        if (dev.model->asic_type == AsicType::GL845 ||
            dev.model->asic_type == AsicType::GL846)
        {
            for (unsigned i = 0; i < 3; i++) {
                if (avg[i] < bottom[i]) {
                    if (avg[i] != 0) {
                        exp[i] = (exp[i] * bottom[i]) / avg[i];
                    } else {
                        exp[i] *= 10;
                    }
                    acceptable = false;
                }
                if (avg[i] > top[i]) {
                    if (avg[i] != 0) {
                        exp[i] = (exp[i] * top[i]) / avg[i];
                    } else {
                        exp[i] *= 10;
                    }
                    acceptable = false;
                }
            }
        } else if (dev.model->asic_type == AsicType::GL847) {
            for (unsigned i = 0; i < 3; i++) {
                if (avg[i] < bottom[i] || avg[i] > top[i]) {
                    auto target = (bottom[i] + top[i]) / 2;
                    if (avg[i] != 0) {
                        exp[i] = (exp[i] * target) / avg[i];
                    } else {
                        exp[i] *= 10;
                    }

                    acceptable = false;
                }
            }
        } else if (dev.model->asic_type == AsicType::GL841 ||
                   dev.model->asic_type == AsicType::GL124)
        {
            for (unsigned i = 0; i < 3; i++) {
                // we accept +- 2% delta from target
                if (std::abs(avg[i] - target) > target / 50) {
                    float prev_weight = 0.5;
                    if (avg[i] != 0) {
                        exp[i] = exp[i] * prev_weight + ((exp[i] * target) / avg[i]) * (1 - prev_weight);
                    } else {
                        exp[i] = exp[i] * prev_weight + (exp[i] * 10) * (1 - prev_weight);
                    }
                    acceptable = false;
                }
            }
        }
    }

    if (dev.model->asic_type == AsicType::GL845 ||
        dev.model->asic_type == AsicType::GL846 ||
        dev.model->asic_type == AsicType::GL847 ||
        dev.model->asic_type == AsicType::GL124)
    {
        // set these values as final ones for scan
        regs_set_exposure(dev.model->asic_type, dev.reg, { exp[0], exp[1], exp[2] });
    }

    if (dev.model->asic_type == AsicType::GL841 ||
        dev.model->asic_type == AsicType::GL842 ||
        dev.model->asic_type == AsicType::GL843)
    {
        dev.cmd_set->move_back_home(&dev, true);
    }

    if (dev.model->asic_type == AsicType::GL845 ||
        dev.model->asic_type == AsicType::GL846 ||
        dev.model->asic_type == AsicType::GL847)
    {
        if (move > 20) {
            dev.cmd_set->move_back_home(&dev, true);
        }
    }

    dbg.vlog(DBG_info,"acceptable exposure: %d, %d, %d\n", exp[0], exp[1], exp[2]);

    return { exp[0], exp[1], exp[2] };
}

void sanei_genesys_calculate_zmod(bool two_table,
                                  uint32_t exposure_time,
                                  const std::vector<uint16_t>& slope_table,
                                  unsigned acceleration_steps,
                                  unsigned move_steps,
                                  unsigned buffer_acceleration_steps,
                                  uint32_t* out_z1, uint32_t* out_z2)
{
    // acceleration total time
    unsigned sum = std::accumulate(slope_table.begin(), slope_table.begin() + acceleration_steps,
                                   0, std::plus<unsigned>());

    /* Z1MOD:
        c = sum(slope_table; reg_stepno)
        d = reg_fwdstep * <cruising speed>
        Z1MOD = (c+d) % exposure_time
    */
    *out_z1 = (sum + buffer_acceleration_steps * slope_table[acceleration_steps - 1]) % exposure_time;

    /* Z2MOD:
        a = sum(slope_table; reg_stepno)
        b = move_steps or 1 if 2 tables
        Z1MOD = (a+b) % exposure_time
    */
    if (!two_table) {
        sum = sum + (move_steps * slope_table[acceleration_steps - 1]);
    } else {
        sum = sum + slope_table[acceleration_steps - 1];
    }
    *out_z2 = sum % exposure_time;
}

/**
 * scans a white area with motor and lamp off to get the per CCD pixel offset
 * that will be used to compute shading coefficient
 * @param dev scanner's device
 */
static void genesys_shading_calibration_impl(Genesys_Device* dev, const Genesys_Sensor& sensor,
                                             Genesys_Register_Set& local_reg,
                                             std::vector<std::uint16_t>& out_average_data,
                                             bool is_dark, const std::string& log_filename_prefix)
{
    DBG_HELPER(dbg);

    if (dev->model->asic_type == AsicType::GL646) {
        dev->cmd_set->init_regs_for_shading(dev, sensor, local_reg);
        local_reg = dev->reg;
    } else {
        local_reg = dev->reg;
        dev->cmd_set->init_regs_for_shading(dev, sensor, local_reg);
        dev->interface->write_registers(local_reg);
    }

    debug_dump(DBG_info, dev->calib_session);

  size_t size;
  uint32_t pixels_per_line;

    if (dev->model->asic_type == AsicType::GL842 ||
        dev->model->asic_type == AsicType::GL843 ||
        dev->model->model_id == ModelId::CANON_5600F)
    {
        pixels_per_line = dev->calib_session.output_pixels;
    } else {
        // BUG: this selects incorrect pixel number
        pixels_per_line = dev->calib_session.params.pixels;
    }
    unsigned channels = dev->calib_session.params.channels;

    // BUG: we are using wrong pixel number here
    unsigned start_offset =
            dev->calib_session.params.startx * sensor.full_resolution / dev->calib_session.params.xres;
    unsigned out_pixels_per_line = pixels_per_line + start_offset;

    // FIXME: we set this during both dark and white calibration. A cleaner approach should
    // probably be used
    dev->average_size = channels * out_pixels_per_line;

    out_average_data.clear();
    out_average_data.resize(dev->average_size);

    if (is_dark && dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) {
        // FIXME: dark shading currently not supported on infrared transparency scans
        return;
    }

    // FIXME: the current calculation is likely incorrect on non-GL843 implementations,
    // but this needs checking. Note the extra line when computing size.
    if (dev->model->asic_type == AsicType::GL842 ||
        dev->model->asic_type == AsicType::GL843 ||
        dev->model->model_id == ModelId::CANON_5600F)
    {
        size = dev->calib_session.output_total_bytes_raw;
    } else {
        size = channels * 2 * pixels_per_line * (dev->calib_session.params.lines + 1);
    }

  std::vector<uint16_t> calibration_data(size / 2);

    // turn off motor and lamp power for flatbed scanners, but not for sheetfed scanners
    // because they have a calibration sheet with a sufficient black strip
    if (is_dark && !dev->model->is_sheetfed) {
        sanei_genesys_set_lamp_power(dev, sensor, local_reg, false);
    } else {
        sanei_genesys_set_lamp_power(dev, sensor, local_reg, true);
    }
    sanei_genesys_set_motor_power(local_reg, true);

    dev->interface->write_registers(local_reg);

    if (is_dark) {
        // wait some time to let lamp to get dark
        dev->interface->sleep_ms(200);
    } else if (has_flag(dev->model->flags, ModelFlag::DARK_CALIBRATION)) {
        // make sure lamp is bright again
        // FIXME: what about scanners that take a long time to warm the lamp?
        dev->interface->sleep_ms(500);
    }

    bool start_motor = !is_dark;
    dev->cmd_set->begin_scan(dev, sensor, &local_reg, start_motor);


    if (is_testing_mode()) {
        dev->interface->test_checkpoint(is_dark ? "dark_shading_calibration"
                                                : "white_shading_calibration");
        dev->cmd_set->end_scan(dev, &local_reg, true);
        return;
    }

    sanei_genesys_read_data_from_scanner(dev, reinterpret_cast<std::uint8_t*>(calibration_data.data()),
                                         size);

    dev->cmd_set->end_scan(dev, &local_reg, true);

    if (has_flag(dev->model->flags, ModelFlag::SWAP_16BIT_DATA)) {
        for (std::size_t i = 0; i < size / 2; ++i) {
            auto value = calibration_data[i];
            value = ((value >> 8) & 0xff) | ((value << 8) & 0xff00);
            calibration_data[i] = value;
        }
    }

    if (has_flag(dev->model->flags, ModelFlag::INVERT_PIXEL_DATA)) {
        for (std::size_t i = 0; i < size / 2; ++i) {
            calibration_data[i] = 0xffff - calibration_data[i];
        }
    }

    std::fill(out_average_data.begin(),
              out_average_data.begin() + start_offset * channels, 0);

    compute_array_percentile_approx(out_average_data.data() +
                                        start_offset * channels,
                                    calibration_data.data(),
                                    dev->calib_session.params.lines, pixels_per_line * channels,
                                    0.5f);

    if (dbg_log_image_data()) {
        write_tiff_file(log_filename_prefix + "_shading.tiff", calibration_data.data(), 16,
                        channels, pixels_per_line, dev->calib_session.params.lines);
        write_tiff_file(log_filename_prefix + "_average.tiff", out_average_data.data(), 16,
                        channels, out_pixels_per_line, 1);
    }
}

/*
 * this function builds dummy dark calibration data so that we can
 * compute shading coefficient in a clean way
 *  todo: current values are hardcoded, we have to find if they
 * can be computed from previous calibration data (when doing offset
 * calibration ?)
 */
static void genesys_dark_shading_by_dummy_pixel(Genesys_Device* dev, const Genesys_Sensor& sensor)
{
    DBG_HELPER(dbg);
  uint32_t pixels_per_line;
  uint32_t skip, xend;
  int dummy1, dummy2, dummy3;	/* dummy black average per channel */

    if (dev->model->asic_type == AsicType::GL842 ||
        dev->model->asic_type == AsicType::GL843)
    {
        pixels_per_line = dev->calib_session.output_pixels;
    } else {
        pixels_per_line = dev->calib_session.params.pixels;
    }

    unsigned channels = dev->calib_session.params.channels;

    // BUG: we are using wrong pixel number here
    unsigned start_offset =
            dev->calib_session.params.startx * sensor.full_resolution / dev->calib_session.params.xres;

    unsigned out_pixels_per_line = pixels_per_line + start_offset;

    dev->average_size = channels * out_pixels_per_line;
  dev->dark_average_data.clear();
  dev->dark_average_data.resize(dev->average_size, 0);

  /* we average values on 'the left' where CCD pixels are under casing and
     give darkest values. We then use these as dummy dark calibration */
    if (dev->settings.xres <= sensor.full_resolution / 2) {
      skip = 4;
      xend = 36;
    }
  else
    {
      skip = 4;
      xend = 68;
    }
    if (dev->model->sensor_id==SensorId::CCD_G4050 ||
        dev->model->sensor_id==SensorId::CCD_HP_4850C
     || dev->model->sensor_id==SensorId::CCD_CANON_4400F
     || dev->model->sensor_id==SensorId::CCD_CANON_8400F
     || dev->model->sensor_id==SensorId::CCD_KVSS080)
    {
      skip = 2;
      xend = sensor.black_pixels;
    }

  /* average each channels on half left margin */
  dummy1 = 0;
  dummy2 = 0;
  dummy3 = 0;

    for (unsigned x = skip + 1; x <= xend; x++) {
        dummy1 += dev->white_average_data[channels * x];
        if (channels > 1) {
            dummy2 += dev->white_average_data[channels * x + 1];
            dummy3 += dev->white_average_data[channels * x + 2];
        }
    }

  dummy1 /= (xend - skip);
  if (channels > 1)
    {
      dummy2 /= (xend - skip);
      dummy3 /= (xend - skip);
    }
  DBG(DBG_proc, "%s: dummy1=%d, dummy2=%d, dummy3=%d \n", __func__, dummy1, dummy2, dummy3);

  /* fill dark_average */
    for (unsigned x = 0; x < out_pixels_per_line; x++) {
        dev->dark_average_data[channels * x] = dummy1;
        if (channels > 1) {
            dev->dark_average_data[channels * x + 1] = dummy2;
            dev->dark_average_data[channels * x + 2] = dummy3;
        }
    }
}

static void genesys_dark_shading_by_constant(Genesys_Device& dev)
{
    dev.dark_average_data.clear();
    dev.dark_average_data.resize(dev.average_size, 0x0101);
}

static void genesys_repark_sensor_before_shading(Genesys_Device* dev)
{
    DBG_HELPER(dbg);
    if (has_flag(dev->model->flags, ModelFlag::SHADING_REPARK)) {
        dev->cmd_set->move_back_home(dev, true);

        if (dev->settings.scan_method == ScanMethod::TRANSPARENCY ||
            dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED)
        {
            scanner_move_to_ta(*dev);
        }
    }
}

static void genesys_repark_sensor_after_white_shading(Genesys_Device* dev)
{
    DBG_HELPER(dbg);
    if (has_flag(dev->model->flags, ModelFlag::SHADING_REPARK)) {
        dev->cmd_set->move_back_home(dev, true);
    }
}

static void genesys_host_shading_calibration_impl(Genesys_Device& dev, const Genesys_Sensor& sensor,
                                                  std::vector<std::uint16_t>& out_average_data,
                                                  bool is_dark,
                                                  const std::string& log_filename_prefix)
{
    DBG_HELPER(dbg);

    if (is_dark && dev.settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) {
        // FIXME: dark shading currently not supported on infrared transparency scans
        return;
    }

    auto local_reg = dev.reg;
    dev.cmd_set->init_regs_for_shading(&dev, sensor, local_reg);

    auto& session = dev.calib_session;
    debug_dump(DBG_info, session);

    // turn off motor and lamp power for flatbed scanners, but not for sheetfed scanners
    // because they have a calibration sheet with a sufficient black strip
    if (is_dark && !dev.model->is_sheetfed) {
        sanei_genesys_set_lamp_power(&dev, sensor, local_reg, false);
    } else {
        sanei_genesys_set_lamp_power(&dev, sensor, local_reg, true);
    }
    sanei_genesys_set_motor_power(local_reg, true);

    dev.interface->write_registers(local_reg);

    if (is_dark) {
        // wait some time to let lamp to get dark
        dev.interface->sleep_ms(200);
    } else if (has_flag(dev.model->flags, ModelFlag::DARK_CALIBRATION)) {
        // make sure lamp is bright again
        // FIXME: what about scanners that take a long time to warm the lamp?
        dev.interface->sleep_ms(500);
    }

    bool start_motor = !is_dark;
    dev.cmd_set->begin_scan(&dev, sensor, &local_reg, start_motor);

    if (is_testing_mode()) {
        dev.interface->test_checkpoint(is_dark ? "host_dark_shading_calibration"
                                               : "host_white_shading_calibration");
        dev.cmd_set->end_scan(&dev, &local_reg, true);
        return;
    }

    Image image = read_unshuffled_image_from_scanner(&dev, session, session.output_total_bytes_raw);
    scanner_stop_action(dev);

    auto start_offset = session.params.startx;
    auto out_pixels_per_line = start_offset + session.output_pixels;

    // FIXME: we set this during both dark and white calibration. A cleaner approach should
    // probably be used
    dev.average_size = session.params.channels * out_pixels_per_line;

    out_average_data.clear();
    out_average_data.resize(dev.average_size);

    std::fill(out_average_data.begin(),
              out_average_data.begin() + start_offset * session.params.channels, 0);

    compute_array_percentile_approx(out_average_data.data() +
                                        start_offset * session.params.channels,
                                    reinterpret_cast<std::uint16_t*>(image.get_row_ptr(0)),
                                    session.params.lines,
                                    session.output_pixels * session.params.channels,
                                    0.5f);

    if (dbg_log_image_data()) {
        write_tiff_file(log_filename_prefix + "_host_shading.tiff", image);
        write_tiff_file(log_filename_prefix + "_host_average.tiff", out_average_data.data(), 16,
                        session.params.channels, out_pixels_per_line, 1);
    }
}

static void genesys_dark_shading_calibration(Genesys_Device* dev, const Genesys_Sensor& sensor,
                                             Genesys_Register_Set& local_reg)
{
    DBG_HELPER(dbg);
    if (has_flag(dev->model->flags, ModelFlag::HOST_SIDE_CALIBRATION_COMPLETE_SCAN)) {
        genesys_host_shading_calibration_impl(*dev, sensor, dev->dark_average_data, true,
                                              "gl_black");
    } else {
        genesys_shading_calibration_impl(dev, sensor, local_reg, dev->dark_average_data, true,
                                         "gl_black");
    }
}

static void genesys_white_shading_calibration(Genesys_Device* dev, const Genesys_Sensor& sensor,
                                              Genesys_Register_Set& local_reg)
{
    DBG_HELPER(dbg);
    if (has_flag(dev->model->flags, ModelFlag::HOST_SIDE_CALIBRATION_COMPLETE_SCAN)) {
        genesys_host_shading_calibration_impl(*dev, sensor, dev->white_average_data, false,
                                              "gl_white");
    } else {
        genesys_shading_calibration_impl(dev, sensor, local_reg, dev->white_average_data, false,
                                         "gl_white");
    }
}

// This calibration uses a scan over the calibration target, comprising a black and a white strip.
// (So the motor must be on.)
static void genesys_dark_white_shading_calibration(Genesys_Device* dev,
                                                   const Genesys_Sensor& sensor,
                                                   Genesys_Register_Set& local_reg)
{
    DBG_HELPER(dbg);

    if (dev->model->asic_type == AsicType::GL646) {
        dev->cmd_set->init_regs_for_shading(dev, sensor, local_reg);
        local_reg = dev->reg;
    } else {
        local_reg = dev->reg;
        dev->cmd_set->init_regs_for_shading(dev, sensor, local_reg);
        dev->interface->write_registers(local_reg);
    }

  size_t size;
  uint32_t pixels_per_line;
  unsigned int x;
  uint32_t dark, white, dark_sum, white_sum, dark_count, white_count, col,
    dif;

    if (dev->model->asic_type == AsicType::GL842 ||
        dev->model->asic_type == AsicType::GL843)
    {
        pixels_per_line = dev->calib_session.output_pixels;
    } else {
        pixels_per_line = dev->calib_session.params.pixels;
    }

    unsigned channels = dev->calib_session.params.channels;

    // BUG: we are using wrong pixel number here
    unsigned start_offset =
            dev->calib_session.params.startx * sensor.full_resolution / dev->calib_session.params.xres;

    unsigned out_pixels_per_line = pixels_per_line + start_offset;

    dev->average_size = channels * out_pixels_per_line;

  dev->white_average_data.clear();
  dev->white_average_data.resize(dev->average_size);

  dev->dark_average_data.clear();
  dev->dark_average_data.resize(dev->average_size);

    if (dev->model->asic_type == AsicType::GL842 ||
        dev->model->asic_type == AsicType::GL843)
    {
        size = dev->calib_session.output_total_bytes_raw;
    } else {
        // FIXME: on GL841 this is different than dev->calib_session.output_total_bytes_raw,
        // needs checking
        size = channels * 2 * pixels_per_line * dev->calib_session.params.lines;
    }

  std::vector<uint8_t> calibration_data(size);

    // turn on motor and lamp power
    sanei_genesys_set_lamp_power(dev, sensor, local_reg, true);
    sanei_genesys_set_motor_power(local_reg, true);

    dev->interface->write_registers(local_reg);

    dev->cmd_set->begin_scan(dev, sensor, &local_reg, false);

    if (is_testing_mode()) {
        dev->interface->test_checkpoint("dark_white_shading_calibration");
        dev->cmd_set->end_scan(dev, &local_reg, true);
        return;
    }

    sanei_genesys_read_data_from_scanner(dev, calibration_data.data(), size);

    dev->cmd_set->end_scan(dev, &local_reg, true);

    if (dbg_log_image_data()) {
        if (dev->model->is_cis) {
            write_tiff_file("gl_black_white_shading.tiff", calibration_data.data(),
                            16, 1, pixels_per_line*channels,
                            dev->calib_session.params.lines);
        } else {
            write_tiff_file("gl_black_white_shading.tiff", calibration_data.data(),
                            16, channels, pixels_per_line,
                            dev->calib_session.params.lines);
        }
    }


    std::fill(dev->dark_average_data.begin(),
              dev->dark_average_data.begin() + start_offset * channels, 0);
    std::fill(dev->white_average_data.begin(),
              dev->white_average_data.begin() + start_offset * channels, 0);

    uint16_t* average_white = dev->white_average_data.data() +
                              start_offset * channels;
    uint16_t* average_dark = dev->dark_average_data.data() +
                             start_offset * channels;

  for (x = 0; x < pixels_per_line * channels; x++)
    {
      dark = 0xffff;
      white = 0;

            for (std::size_t y = 0; y < dev->calib_session.params.lines; y++)
	{
	  col = calibration_data[(x + y * pixels_per_line * channels) * 2];
	  col |=
	    calibration_data[(x + y * pixels_per_line * channels) * 2 +
			     1] << 8;

	  if (col > white)
	    white = col;
	  if (col < dark)
	    dark = col;
	}

      dif = white - dark;

      dark = dark + dif / 8;
      white = white - dif / 8;

      dark_count = 0;
      dark_sum = 0;

      white_count = 0;
      white_sum = 0;

            for (std::size_t y = 0; y < dev->calib_session.params.lines; y++)
	{
	  col = calibration_data[(x + y * pixels_per_line * channels) * 2];
	  col |=
	    calibration_data[(x + y * pixels_per_line * channels) * 2 +
			     1] << 8;

	  if (col >= white)
	    {
	      white_sum += col;
	      white_count++;
	    }
	  if (col <= dark)
	    {
	      dark_sum += col;
	      dark_count++;
	    }

	}

      dark_sum /= dark_count;
      white_sum /= white_count;

        *average_dark++ = dark_sum;
        *average_white++ = white_sum;
    }

    if (dbg_log_image_data()) {
        write_tiff_file("gl_white_average.tiff", dev->white_average_data.data(), 16, channels,
                        out_pixels_per_line, 1);
        write_tiff_file("gl_dark_average.tiff", dev->dark_average_data.data(), 16, channels,
                        out_pixels_per_line, 1);
    }
}

/* computes one coefficient given bright-dark value
 * @param coeff factor giving 1.00 gain
 * @param target desired target code
 * @param value brght-dark value
 * */
static unsigned int
compute_coefficient (unsigned int coeff, unsigned int target, unsigned int value)
{
  int result;

  if (value > 0)
    {
      result = (coeff * target) / value;
      if (result >= 65535)
	{
	  result = 65535;
	}
    }
  else
    {
      result = coeff;
    }
  return result;
}

/** @brief compute shading coefficients for LiDE scanners
 * The dark/white shading is actually performed _after_ reducing
 * resolution via averaging. only dark/white shading data for what would be
 * first pixel at full resolution is used.
 *
 * scanner raw input to output value calculation:
 *   o=(i-off)*(gain/coeff)
 *
 * from datasheet:
 *   off=dark_average
 *   gain=coeff*bright_target/(bright_average-dark_average)
 * works for dark_target==0
 *
 * what we want is these:
 *   bright_target=(bright_average-off)*(gain/coeff)
 *   dark_target=(dark_average-off)*(gain/coeff)
 * leading to
 *  off = (dark_average*bright_target - bright_average*dark_target)/(bright_target - dark_target)
 *  gain = (bright_target - dark_target)/(bright_average - dark_average)*coeff
 *
 * @param dev scanner's device
 * @param shading_data memory area where to store the computed shading coefficients
 * @param pixels_per_line number of pixels per line
 * @param words_per_color memory words per color channel
 * @param channels number of color channels (actually 1 or 3)
 * @param o shading coefficients left offset
 * @param coeff 4000h or 2000h depending on fast scan mode or not (GAIN4 bit)
 * @param target_bright value of the white target code
 * @param target_dark value of the black target code
*/
static void
compute_averaged_planar (Genesys_Device * dev, const Genesys_Sensor& sensor,
			 uint8_t * shading_data,
			 unsigned int pixels_per_line,
			 unsigned int words_per_color,
			 unsigned int channels,
			 unsigned int o,
			 unsigned int coeff,
			 unsigned int target_bright,
			 unsigned int target_dark)
{
  unsigned int x, i, j, br, dk, res, avgpixels, basepixels, val;
  unsigned int fill,factor;

  DBG(DBG_info, "%s: pixels=%d, offset=%d\n", __func__, pixels_per_line, o);

  /* initialize result */
  memset (shading_data, 0xff, words_per_color * 3 * 2);

  /*
     strangely i can write 0x20000 bytes beginning at 0x00000 without overwriting
     slope tables - which begin at address 0x10000(for 1200dpi hw mode):
     memory is organized in words(2 bytes) instead of single bytes. explains
     quite some things
   */
/*
  another one: the dark/white shading is actually performed _after_ reducing
  resolution via averaging. only dark/white shading data for what would be
  first pixel at full resolution is used.
 */
/*
  scanner raw input to output value calculation:
    o=(i-off)*(gain/coeff)

  from datasheet:
    off=dark_average
    gain=coeff*bright_target/(bright_average-dark_average)
  works for dark_target==0

  what we want is these:
    bright_target=(bright_average-off)*(gain/coeff)
    dark_target=(dark_average-off)*(gain/coeff)
  leading to
    off = (dark_average*bright_target - bright_average*dark_target)/(bright_target - dark_target)
    gain = (bright_target - dark_target)/(bright_average - dark_average)*coeff
 */
  res = dev->settings.xres;

    if (sensor.full_resolution > sensor.get_optical_resolution()) {
        res *= 2;
    }

    // this should be evenly dividable
    basepixels = sensor.full_resolution / res;

  /* gl841 supports 1/1 1/2 1/3 1/4 1/5 1/6 1/8 1/10 1/12 1/15 averaging */
  if (basepixels < 1)
    avgpixels = 1;
  else if (basepixels < 6)
    avgpixels = basepixels;
  else if (basepixels < 8)
    avgpixels = 6;
  else if (basepixels < 10)
    avgpixels = 8;
  else if (basepixels < 12)
    avgpixels = 10;
  else if (basepixels < 15)
    avgpixels = 12;
  else
    avgpixels = 15;

  /* LiDE80 packs shading data */
    if (dev->model->sensor_id != SensorId::CIS_CANON_LIDE_80) {
      factor=1;
      fill=avgpixels;
    }
  else
    {
      factor=avgpixels;
      fill=1;
    }

  DBG(DBG_info, "%s: averaging over %d pixels\n", __func__, avgpixels);
  DBG(DBG_info, "%s: packing factor is %d\n", __func__, factor);
  DBG(DBG_info, "%s: fill length is %d\n", __func__, fill);

  for (x = 0; x <= pixels_per_line - avgpixels; x += avgpixels)
    {
      if ((x + o) * 2 * 2 + 3 > words_per_color * 2)
	break;

      for (j = 0; j < channels; j++)
	{

	  dk = 0;
	  br = 0;
	  for (i = 0; i < avgpixels; i++)
	    {
                // dark data
                dk += dev->dark_average_data[(x + i + pixels_per_line * j)];
                // white data
                br += dev->white_average_data[(x + i + pixels_per_line * j)];
	    }

	  br /= avgpixels;
	  dk /= avgpixels;

	  if (br * target_dark > dk * target_bright)
	    val = 0;
	  else if (dk * target_bright - br * target_dark >
		   65535 * (target_bright - target_dark))
	    val = 65535;
	  else
            {
	      val = (dk * target_bright - br * target_dark) / (target_bright - target_dark);
            }

          /*fill all pixels, even if only the last one is relevant*/
	  for (i = 0; i < fill; i++)
	    {
	      shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j] = val & 0xff;
	      shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 1] = val >> 8;
	    }

	  val = br - dk;

	  if (65535 * val > (target_bright - target_dark) * coeff)
            {
	      val = (coeff * (target_bright - target_dark)) / val;
            }
	  else
            {
	      val = 65535;
            }

          /*fill all pixels, even if only the last one is relevant*/
	  for (i = 0; i < fill; i++)
	    {
	      shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 2] = val & 0xff;
	      shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 3] = val >> 8;
	    }
	}

      /* fill remaining channels */
      for (j = channels; j < 3; j++)
	{
	  for (i = 0; i < fill; i++)
	    {
	      shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j    ] = shading_data[(x/factor + o + i) * 2 * 2    ];
	      shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 1] = shading_data[(x/factor + o + i) * 2 * 2 + 1];
	      shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 2] = shading_data[(x/factor + o + i) * 2 * 2 + 2];
	      shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 3] = shading_data[(x/factor + o + i) * 2 * 2 + 3];
	    }
	}
    }
}

static std::array<unsigned, 3> color_order_to_cmat(ColorOrder color_order)
{
    switch (color_order) {
        case ColorOrder::RGB: return {0, 1, 2};
        case ColorOrder::GBR: return {2, 0, 1};
        default:
            throw std::logic_error("Unknown color order");
    }
}

/**
 * Computes shading coefficient using formula in data sheet. 16bit data values
 * manipulated here are little endian. For now we assume deletion scanning type
 * and that there is always 3 channels.
 * @param dev scanner's device
 * @param shading_data memory area where to store the computed shading coefficients
 * @param pixels_per_line number of pixels per line
 * @param channels number of color channels (actually 1 or 3)
 * @param cmat color transposition matrix
 * @param offset shading coefficients left offset
 * @param coeff 4000h or 2000h depending on fast scan mode or not
 * @param target value of the target code
 */
static void compute_coefficients(Genesys_Device * dev,
		      uint8_t * shading_data,
		      unsigned int pixels_per_line,
		      unsigned int channels,
                                 ColorOrder color_order,
		      int offset,
		      unsigned int coeff,
		      unsigned int target)
{
  uint8_t *ptr;			/* contain 16bit words in little endian */
  unsigned int x, c;
  unsigned int val, br, dk;
  unsigned int start, end;

  DBG(DBG_io, "%s: pixels_per_line=%d,  coeff=0x%04x\n", __func__, pixels_per_line, coeff);

    auto cmat = color_order_to_cmat(color_order);

  /* compute start & end values depending of the offset */
  if (offset < 0)
   {
      start = -1 * offset;
      end = pixels_per_line;
   }
  else
   {
     start = 0;
     end = pixels_per_line - offset;
   }

  for (c = 0; c < channels; c++)
    {
      for (x = start; x < end; x++)
	{
	  /* TODO if channels=1 , use filter to know the base addr */
	  ptr = shading_data + 4 * ((x + offset) * channels + cmat[c]);

        // dark data
        dk = dev->dark_average_data[x * channels + c];

        // white data
        br = dev->white_average_data[x * channels + c];

	  /* compute coeff */
	  val=compute_coefficient(coeff,target,br-dk);

	  /* assign it */
	  ptr[0] = dk & 255;
	  ptr[1] = dk / 256;
	  ptr[2] = val & 0xff;
	  ptr[3] = val / 256;

	}
    }
}

/**
 * Computes shading coefficient using formula in data sheet. 16bit data values
 * manipulated here are little endian. Data is in planar form, ie grouped by
 * lines of the same color component.
 * @param dev scanner's device
 * @param shading_data memory area where to store the computed shading coefficients
 * @param factor averaging factor when the calibration scan is done at a higher resolution
 * than the final scan
 * @param pixels_per_line number of pixels per line
 * @param words_per_color total number of shading data words for one color element
 * @param channels number of color channels (actually 1 or 3)
 * @param cmat transcoding matrix for color channel order
 * @param offset shading coefficients left offset
 * @param coeff 4000h or 2000h depending on fast scan mode or not
 * @param target white target value
 */
static void compute_planar_coefficients(Genesys_Device * dev,
			     uint8_t * shading_data,
			     unsigned int factor,
			     unsigned int pixels_per_line,
			     unsigned int words_per_color,
			     unsigned int channels,
                                        ColorOrder color_order,
			     unsigned int offset,
			     unsigned int coeff,
			     unsigned int target)
{
  uint8_t *ptr;			/* contains 16bit words in little endian */
  uint32_t x, c, i;
  uint32_t val, dk, br;

    auto cmat = color_order_to_cmat(color_order);

  DBG(DBG_io, "%s: factor=%d, pixels_per_line=%d, words=0x%X, coeff=0x%04x\n", __func__, factor,
      pixels_per_line, words_per_color, coeff);
  for (c = 0; c < channels; c++)
    {
      /* shading data is larger than pixels_per_line so offset can be neglected */
      for (x = 0; x < pixels_per_line; x+=factor)
	{
	  /* x2 because of 16 bit values, and x2 since one coeff for dark
	   * and another for white */
	  ptr = shading_data + words_per_color * cmat[c] * 2 + (x + offset) * 4;

	  dk = 0;
	  br = 0;

	  /* average case */
	  for(i=0;i<factor;i++)
	  {
                dk += dev->dark_average_data[((x+i) + pixels_per_line * c)];
                br += dev->white_average_data[((x+i) + pixels_per_line * c)];
	  }
	  dk /= factor;
	  br /= factor;

	  val = compute_coefficient (coeff, target, br - dk);

	  /* we duplicate the information to have calibration data at optical resolution */
	  for (i = 0; i < factor; i++)
	    {
	      ptr[0 + 4 * i] = dk & 255;
	      ptr[1 + 4 * i] = dk / 256;
	      ptr[2 + 4 * i] = val & 0xff;
	      ptr[3 + 4 * i] = val / 256;
	    }
	}
    }
  /* in case of gray level scan, we duplicate shading information on all
   * three color channels */
  if(channels==1)
  {
	  memcpy(shading_data+cmat[1]*2*words_per_color,
	         shading_data+cmat[0]*2*words_per_color,
		 words_per_color*2);
	  memcpy(shading_data+cmat[2]*2*words_per_color,
	         shading_data+cmat[0]*2*words_per_color,
		 words_per_color*2);
  }
}

static void
compute_shifted_coefficients (Genesys_Device * dev,
                              const Genesys_Sensor& sensor,
			      uint8_t * shading_data,
			      unsigned int pixels_per_line,
			      unsigned int channels,
                              ColorOrder color_order,
			      int offset,
			      unsigned int coeff,
			      unsigned int target_dark,
			      unsigned int target_bright,
			      unsigned int patch_size)		/* contiguous extent */
{
  unsigned int x, avgpixels, basepixels, i, j, val1, val2;
  unsigned int br_tmp [3], dk_tmp [3];
  uint8_t *ptr = shading_data + offset * 3 * 4;                 /* contain 16bit words in little endian */
  unsigned int patch_cnt = offset * 3;                          /* at start, offset of first patch */

    auto cmat = color_order_to_cmat(color_order);

  x = dev->settings.xres;
    if (sensor.full_resolution > sensor.get_optical_resolution()) {
        x *= 2;	// scanner is using half-ccd mode
    }
    basepixels = sensor.full_resolution / x; // this should be evenly dividable

      /* gl841 supports 1/1 1/2 1/3 1/4 1/5 1/6 1/8 1/10 1/12 1/15 averaging */
      if (basepixels < 1)
        avgpixels = 1;
      else if (basepixels < 6)
        avgpixels = basepixels;
      else if (basepixels < 8)
        avgpixels = 6;
      else if (basepixels < 10)
        avgpixels = 8;
      else if (basepixels < 12)
        avgpixels = 10;
      else if (basepixels < 15)
        avgpixels = 12;
      else
        avgpixels = 15;
  DBG(DBG_info, "%s: pixels_per_line=%d,  coeff=0x%04x,  averaging over %d pixels\n", __func__,
      pixels_per_line, coeff, avgpixels);

  for (x = 0; x <= pixels_per_line - avgpixels; x += avgpixels) {
    memset (&br_tmp, 0, sizeof(br_tmp));
    memset (&dk_tmp, 0, sizeof(dk_tmp));

    for (i = 0; i < avgpixels; i++) {
      for (j = 0; j < channels; j++) {
                br_tmp[j] += dev->white_average_data[((x + i) * channels + j)];
                dk_tmp[i] += dev->dark_average_data[((x + i) * channels + j)];
      }
    }
    for (j = 0; j < channels; j++) {
      br_tmp[j] /= avgpixels;
      dk_tmp[j] /= avgpixels;

      if (br_tmp[j] * target_dark > dk_tmp[j] * target_bright)
        val1 = 0;
      else if (dk_tmp[j] * target_bright - br_tmp[j] * target_dark > 65535 * (target_bright - target_dark))
        val1 = 65535;
      else
        val1 = (dk_tmp[j] * target_bright - br_tmp[j] * target_dark) / (target_bright - target_dark);

      val2 = br_tmp[j] - dk_tmp[j];
      if (65535 * val2 > (target_bright - target_dark) * coeff)
        val2 = (coeff * (target_bright - target_dark)) / val2;
      else
        val2 = 65535;

      br_tmp[j] = val1;
      dk_tmp[j] = val2;
    }
    for (i = 0; i < avgpixels; i++) {
      for (j = 0; j < channels; j++) {
        * ptr++ = br_tmp[ cmat[j] ] & 0xff;
        * ptr++ = br_tmp[ cmat[j] ] >> 8;
        * ptr++ = dk_tmp[ cmat[j] ] & 0xff;
        * ptr++ = dk_tmp[ cmat[j] ] >> 8;
        patch_cnt++;
        if (patch_cnt == patch_size) {
          patch_cnt = 0;
          val1 = cmat[2];
          cmat[2] = cmat[1];
          cmat[1] = cmat[0];
          cmat[0] = val1;
        }
      }
    }
  }
}

static void genesys_send_shading_coefficient(Genesys_Device* dev, const Genesys_Sensor& sensor)
{
    DBG_HELPER(dbg);

    if (sensor.use_host_side_calib) {
        return;
    }

  uint32_t pixels_per_line;
  int o;
  unsigned int length;		/**> number of shading calibration data words */
  unsigned int factor;
  unsigned int coeff, target_code, words_per_color = 0;


    // BUG: we are using wrong pixel number here
    unsigned start_offset =
            dev->calib_session.params.startx * sensor.full_resolution / dev->calib_session.params.xres;

    if (dev->model->asic_type == AsicType::GL842 ||
        dev->model->asic_type == AsicType::GL843)
    {
        pixels_per_line = dev->calib_session.output_pixels + start_offset;
    } else {
        pixels_per_line = dev->calib_session.params.pixels + start_offset;
    }

    unsigned channels = dev->calib_session.params.channels;

  /* we always build data for three channels, even for gray
   * we make the shading data such that each color channel data line is contiguous
   * to the next one, which allow to write the 3 channels in 1 write
   * during genesys_send_shading_coefficient, some values are words, other bytes
   * hence the x2 factor */
  switch (dev->reg.get8(0x05) >> 6)
    {
      /* 600 dpi */
    case 0:
      words_per_color = 0x2a00;
      break;
      /* 1200 dpi */
    case 1:
      words_per_color = 0x5500;
      break;
      /* 2400 dpi */
    case 2:
      words_per_color = 0xa800;
      break;
      /* 4800 dpi */
    case 3:
      words_per_color = 0x15000;
      break;
    }

  /* special case, memory is aligned on 0x5400, this has yet to be explained */
  /* could be 0xa800 because sensor is truly 2400 dpi, then halved because
   * we only set 1200 dpi */
  if(dev->model->sensor_id==SensorId::CIS_CANON_LIDE_80)
    {
      words_per_color = 0x5400;
    }

  length = words_per_color * 3 * 2;

  /* allocate computed size */
  // contains 16bit words in little endian
  std::vector<uint8_t> shading_data(length, 0);

    if (!dev->calib_session.computed) {
        genesys_send_offset_and_shading(dev, sensor, shading_data.data(), length);
        return;
    }

  /* TARGET/(Wn-Dn) = white gain -> ~1.xxx then it is multiplied by 0x2000
     or 0x4000 to give an integer
     Wn = white average for column n
     Dn = dark average for column n
   */
    if (get_registers_gain4_bit(dev->model->asic_type, dev->reg)) {
        coeff = 0x4000;
    } else {
        coeff = 0x2000;
    }

  /* compute avg factor */
    if (dev->settings.xres > sensor.full_resolution) {
        factor = 1;
    } else {
        factor = sensor.full_resolution / dev->settings.xres;
    }

  /* for GL646, shading data is planar if REG_0x01_FASTMOD is set and
   * chunky if not. For now we rely on the fact that we know that
   * each sensor is used only in one mode. Currently only the CIS_XP200
   * sets REG_0x01_FASTMOD.
   */

  /* TODO setup a struct in genesys_devices that
   * will handle these settings instead of having this switch growing up */
  switch (dev->model->sensor_id)
    {
    case SensorId::CCD_XP300:
        case SensorId::CCD_DOCKETPORT_487:
    case SensorId::CCD_ROADWARRIOR:
    case SensorId::CCD_DP665:
    case SensorId::CCD_DP685:
    case SensorId::CCD_DSMOBILE600:
      target_code = 0xdc00;
      o = 4;
      compute_planar_coefficients (dev,
                   shading_data.data(),
				   factor,
				   pixels_per_line,
				   words_per_color,
				   channels,
                   ColorOrder::RGB,
				   o,
				   coeff,
				   target_code);
      break;
    case SensorId::CIS_XP200:
      target_code = 0xdc00;
      o = 2;
      compute_planar_coefficients (dev,
                   shading_data.data(),
				   1,
				   pixels_per_line,
				   words_per_color,
				   channels,
                   ColorOrder::GBR,
				   o,
				   coeff,
				   target_code);
      break;
    case SensorId::CCD_HP2300:
      target_code = 0xdc00;
      o = 2;
            if (dev->settings.xres <= sensor.full_resolution / 2) {
                o = o - sensor.dummy_pixel / 2;
            }
      compute_coefficients (dev,
                shading_data.data(),
			    pixels_per_line,
			    3,
                            ColorOrder::RGB,
                            o,
                            coeff,
                            target_code);
      break;
    case SensorId::CCD_5345:
      target_code = 0xe000;
      o = 4;
      if(dev->settings.xres<=sensor.full_resolution/2)
       {
          o = o - sensor.dummy_pixel;
       }
      compute_coefficients (dev,
                shading_data.data(),
			    pixels_per_line,
			    3,
                            ColorOrder::RGB,
                            o,
                            coeff,
                            target_code);
      break;
    case SensorId::CCD_HP3670:
    case SensorId::CCD_HP2400:
      target_code = 0xe000;
            // offset is dependent on ccd_pixels_per_system_pixel(), but we couldn't use this in
            // common code previously.
            // FIXME: use sensor.ccd_pixels_per_system_pixel()
      if(dev->settings.xres<=300)
        {
                o = -10;
        }
      else if(dev->settings.xres<=600)
        {
                o = -6;
        }
      else
        {
          o = +2;
        }
      compute_coefficients (dev,
                shading_data.data(),
			    pixels_per_line,
			    3,
                            ColorOrder::RGB,
                            o,
                            coeff,
                            target_code);
      break;
    case SensorId::CCD_KVSS080:
    case SensorId::CCD_PLUSTEK_OPTICBOOK_3800:
    case SensorId::CCD_G4050:
        case SensorId::CCD_HP_4850C:
    case SensorId::CCD_CANON_4400F:
    case SensorId::CCD_CANON_8400F:
    case SensorId::CCD_CANON_8600F:
        case SensorId::CCD_PLUSTEK_OPTICFILM_7200:
    case SensorId::CCD_PLUSTEK_OPTICFILM_7200I:
    case SensorId::CCD_PLUSTEK_OPTICFILM_7300:
        case SensorId::CCD_PLUSTEK_OPTICFILM_7400:
    case SensorId::CCD_PLUSTEK_OPTICFILM_7500I:
        case SensorId::CCD_PLUSTEK_OPTICFILM_8200I:
      target_code = 0xe000;
      o = 0;
      compute_coefficients (dev,
                shading_data.data(),
			    pixels_per_line,
			    3,
                            ColorOrder::RGB,
                            o,
                            coeff,
                            target_code);
      break;
    case SensorId::CIS_CANON_LIDE_700F:
    case SensorId::CIS_CANON_LIDE_100:
    case SensorId::CIS_CANON_LIDE_200:
    case SensorId::CIS_CANON_LIDE_110:
    case SensorId::CIS_CANON_LIDE_120:
    case SensorId::CIS_CANON_LIDE_210:
    case SensorId::CIS_CANON_LIDE_220:
        case SensorId::CCD_CANON_5600F:
        /* TODO store this in a data struct so we avoid
         * growing this switch */
        switch(dev->model->sensor_id)
          {
          case SensorId::CIS_CANON_LIDE_110:
          case SensorId::CIS_CANON_LIDE_120:
          case SensorId::CIS_CANON_LIDE_210:
          case SensorId::CIS_CANON_LIDE_220:
          case SensorId::CIS_CANON_LIDE_700F:
                target_code = 0xc000;
            break;
          default:
            target_code = 0xdc00;
          }
        words_per_color=pixels_per_line*2;
        length = words_per_color * 3 * 2;
        shading_data.clear();
        shading_data.resize(length, 0);
        compute_planar_coefficients (dev,
                                     shading_data.data(),
                                     1,
                                     pixels_per_line,
                                     words_per_color,
                                     channels,
                                     ColorOrder::RGB,
                                     0,
                                     coeff,
                                     target_code);
      break;
    case SensorId::CIS_CANON_LIDE_35:
        case SensorId::CIS_CANON_LIDE_60:
            case SensorId::CIS_CANON_LIDE_90:
      compute_averaged_planar (dev, sensor,
                               shading_data.data(),
                               pixels_per_line,
                               words_per_color,
                               channels,
                               4,
                               coeff,
                               0xe000,
                               0x0a00);
      break;
    case SensorId::CIS_CANON_LIDE_80:
      compute_averaged_planar (dev, sensor,
                               shading_data.data(),
                               pixels_per_line,
                               words_per_color,
                               channels,
                               0,
                               coeff,
			       0xe000,
                               0x0800);
      break;
    case SensorId::CCD_PLUSTEK_OPTICPRO_3600:
      compute_shifted_coefficients (dev, sensor,
                        shading_data.data(),
			            pixels_per_line,
			            channels,
                        ColorOrder::RGB,
			            12,         /* offset */
			            coeff,
 			            0x0001,      /* target_dark */
			            0xf900,      /* target_bright */
			            256);        /* patch_size: contiguous extent */
      break;
    default:
        throw SaneException(SANE_STATUS_UNSUPPORTED, "sensor %d not supported",
                            static_cast<unsigned>(dev->model->sensor_id));
      break;
    }

    // do the actual write of shading calibration data to the scanner
    genesys_send_offset_and_shading(dev, sensor, shading_data.data(), length);
}


/**
 * search calibration cache list for an entry matching required scan.
 * If one is found, set device calibration with it
 * @param dev scanner's device
 * @return false if no matching cache entry has been
 * found, true if one has been found and used.
 */
static bool
genesys_restore_calibration(Genesys_Device * dev, Genesys_Sensor& sensor)
{
    DBG_HELPER(dbg);

    // if no cache or no function to evaluate cache entry there can be no match/
    if (dev->calibration_cache.empty()) {
        return false;
    }

    auto session = dev->cmd_set->calculate_scan_session(dev, sensor, dev->settings);

  /* we walk the link list of calibration cache in search for a
   * matching one */
  for (auto& cache : dev->calibration_cache)
    {
        if (sanei_genesys_is_compatible_calibration(dev, session, &cache, false)) {
            dev->frontend = cache.frontend;
          /* we don't restore the gamma fields */
          sensor.exposure = cache.sensor.exposure;

            dev->calib_session = cache.session;
          dev->average_size = cache.average_size;

          dev->dark_average_data = cache.dark_average_data;
          dev->white_average_data = cache.white_average_data;

            if (!dev->cmd_set->has_send_shading_data()) {
            genesys_send_shading_coefficient(dev, sensor);
          }

          DBG(DBG_proc, "%s: restored\n", __func__);
          return true;
	}
    }
  DBG(DBG_proc, "%s: completed(nothing found)\n", __func__);
  return false;
}


static void genesys_save_calibration(Genesys_Device* dev, const Genesys_Sensor& sensor)
{
    DBG_HELPER(dbg);
#ifdef HAVE_SYS_TIME_H
  struct timeval time;
#endif

    auto session = dev->cmd_set->calculate_scan_session(dev, sensor, dev->settings);

  auto found_cache_it = dev->calibration_cache.end();
  for (auto cache_it = dev->calibration_cache.begin(); cache_it != dev->calibration_cache.end();
       cache_it++)
    {
        if (sanei_genesys_is_compatible_calibration(dev, session, &*cache_it, true)) {
            found_cache_it = cache_it;
            break;
        }
    }

  /* if we found on overridable cache, we reuse it */
  if (found_cache_it == dev->calibration_cache.end())
    {
      /* create a new cache entry and insert it in the linked list */
      dev->calibration_cache.push_back(Genesys_Calibration_Cache());
      found_cache_it = std::prev(dev->calibration_cache.end());
    }

  found_cache_it->average_size = dev->average_size;

  found_cache_it->dark_average_data = dev->dark_average_data;
  found_cache_it->white_average_data = dev->white_average_data;

    found_cache_it->params = session.params;
  found_cache_it->frontend = dev->frontend;
  found_cache_it->sensor = sensor;

    found_cache_it->session = dev->calib_session;

#ifdef HAVE_SYS_TIME_H
    gettimeofday(&time, nullptr);
  found_cache_it->last_calibration = time.tv_sec;
#endif
}

static void genesys_flatbed_calibration(Genesys_Device* dev, Genesys_Sensor& sensor)
{
    DBG_HELPER(dbg);
    uint32_t pixels_per_line;

    unsigned coarse_res = sensor.full_resolution;
    if (dev->settings.yres <= sensor.full_resolution / 2) {
        coarse_res /= 2;
    }

    if (dev->model->model_id == ModelId::CANON_8400F) {
        coarse_res = 1600;
    }

    if (dev->model->model_id == ModelId::CANON_4400F ||
        dev->model->model_id == ModelId::CANON_8600F)
    {
        coarse_res = 1200;
    }

    auto local_reg = dev->initial_regs;

    if (!has_flag(dev->model->flags, ModelFlag::DISABLE_ADC_CALIBRATION)) {
        // do ADC calibration first.
        dev->interface->record_progress_message("offset_calibration");
        dev->cmd_set->offset_calibration(dev, sensor, local_reg);

        dev->interface->record_progress_message("coarse_gain_calibration");
        dev->cmd_set->coarse_gain_calibration(dev, sensor, local_reg, coarse_res);
    }

    if (dev->model->is_cis &&
        !has_flag(dev->model->flags, ModelFlag::DISABLE_EXPOSURE_CALIBRATION))
    {
        // ADC now sends correct data, we can configure the exposure for the LEDs
        dev->interface->record_progress_message("led_calibration");
        switch (dev->model->asic_type) {
            case AsicType::GL124:
            case AsicType::GL841:
            case AsicType::GL845:
            case AsicType::GL846:
            case AsicType::GL847: {
                auto calib_exposure = dev->cmd_set->led_calibration(dev, sensor, local_reg);
                for (auto& sensor_update :
                        sanei_genesys_find_sensors_all_for_write(dev, sensor.method)) {
                    sensor_update.get().exposure = calib_exposure;
                }
                sensor.exposure = calib_exposure;
                break;
            }
            default: {
                sensor.exposure = dev->cmd_set->led_calibration(dev, sensor, local_reg);
            }
        }

        if (!has_flag(dev->model->flags, ModelFlag::DISABLE_ADC_CALIBRATION)) {
            // recalibrate ADC again for the new LED exposure
            dev->interface->record_progress_message("offset_calibration");
            dev->cmd_set->offset_calibration(dev, sensor, local_reg);

            dev->interface->record_progress_message("coarse_gain_calibration");
            dev->cmd_set->coarse_gain_calibration(dev, sensor, local_reg, coarse_res);
        }
    }

  /* we always use sensor pixel number when the ASIC can't handle multi-segments sensor */
    if (!has_flag(dev->model->flags, ModelFlag::SIS_SENSOR)) {
        pixels_per_line = static_cast<std::uint32_t>((dev->model->x_size * dev->settings.xres) /
                                                     MM_PER_INCH);
    } else {
        pixels_per_line = static_cast<std::uint32_t>((dev->model->x_size_calib_mm * dev->settings.xres)
                                                      / MM_PER_INCH);
    }

    // send default shading data
    dev->interface->record_progress_message("sanei_genesys_init_shading_data");
    sanei_genesys_init_shading_data(dev, sensor, pixels_per_line);

    if (dev->settings.scan_method == ScanMethod::TRANSPARENCY ||
        dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED)
    {
        scanner_move_to_ta(*dev);
    }

    // shading calibration
    if (!has_flag(dev->model->flags, ModelFlag::DISABLE_SHADING_CALIBRATION)) {
        if (has_flag(dev->model->flags, ModelFlag::DARK_WHITE_CALIBRATION)) {
            dev->interface->record_progress_message("genesys_dark_white_shading_calibration");
            genesys_dark_white_shading_calibration(dev, sensor, local_reg);
        } else {
            DBG(DBG_proc, "%s : genesys_dark_shading_calibration local_reg ", __func__);
            debug_dump(DBG_proc, local_reg);

            if (has_flag(dev->model->flags, ModelFlag::DARK_CALIBRATION)) {
                dev->interface->record_progress_message("genesys_dark_shading_calibration");
                genesys_dark_shading_calibration(dev, sensor, local_reg);
                genesys_repark_sensor_before_shading(dev);
            }

            dev->interface->record_progress_message("genesys_white_shading_calibration");
            genesys_white_shading_calibration(dev, sensor, local_reg);

            genesys_repark_sensor_after_white_shading(dev);

            if (!has_flag(dev->model->flags, ModelFlag::DARK_CALIBRATION)) {
                if (has_flag(dev->model->flags, ModelFlag::USE_CONSTANT_FOR_DARK_CALIBRATION)) {
                    genesys_dark_shading_by_constant(*dev);
                } else {
                    genesys_dark_shading_by_dummy_pixel(dev, sensor);
                }
            }
        }
    }

    if (!dev->cmd_set->has_send_shading_data()) {
        dev->interface->record_progress_message("genesys_send_shading_coefficient");
        genesys_send_shading_coefficient(dev, sensor);
    }
}

/**
 * Does the calibration process for a sheetfed scanner
 * - offset calibration
 * - gain calibration
 * - shading calibration
 * During calibration a predefined calibration sheet with specific black and white
 * areas is used.
 * @param dev device to calibrate
 */
static void genesys_sheetfed_calibration(Genesys_Device* dev, Genesys_Sensor& sensor)
{
    DBG_HELPER(dbg);
    bool forward = true;

    auto local_reg = dev->initial_regs;

    // first step, load document
    dev->cmd_set->load_document(dev);

    unsigned coarse_res = sensor.full_resolution;

  /* the afe needs to sends valid data even before calibration */

  /* go to a white area */
    try {
        scanner_search_strip(*dev, forward, false);
    } catch (...) {
        catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); });
        throw;
    }

    if (!has_flag(dev->model->flags, ModelFlag::DISABLE_ADC_CALIBRATION)) {
        // do ADC calibration first.
        dev->interface->record_progress_message("offset_calibration");
        dev->cmd_set->offset_calibration(dev, sensor, local_reg);

        dev->interface->record_progress_message("coarse_gain_calibration");
        dev->cmd_set->coarse_gain_calibration(dev, sensor, local_reg, coarse_res);
    }

    if (dev->model->is_cis &&
        !has_flag(dev->model->flags, ModelFlag::DISABLE_EXPOSURE_CALIBRATION))
    {
        // ADC now sends correct data, we can configure the exposure for the LEDs
        dev->interface->record_progress_message("led_calibration");
        dev->cmd_set->led_calibration(dev, sensor, local_reg);

        if (!has_flag(dev->model->flags, ModelFlag::DISABLE_ADC_CALIBRATION)) {
            // recalibrate ADC again for the new LED exposure
            dev->interface->record_progress_message("offset_calibration");
            dev->cmd_set->offset_calibration(dev, sensor, local_reg);

            dev->interface->record_progress_message("coarse_gain_calibration");
            dev->cmd_set->coarse_gain_calibration(dev, sensor, local_reg, coarse_res);
        }
    }

  /* search for a full width black strip and then do a 16 bit scan to
   * gather black shading data */
    if (has_flag(dev->model->flags, ModelFlag::DARK_CALIBRATION)) {
        // seek black/white reverse/forward
        try {
            scanner_search_strip(*dev, forward, true);
        } catch (...) {
            catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); });
            throw;
        }

        try {
            genesys_dark_shading_calibration(dev, sensor, local_reg);
        } catch (...) {
            catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); });
            throw;
        }
        forward = false;
    }


  /* go to a white area */
    try {
        scanner_search_strip(*dev, forward, false);
    } catch (...) {
        catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); });
        throw;
    }

  genesys_repark_sensor_before_shading(dev);

    try {
        genesys_white_shading_calibration(dev, sensor, local_reg);
        genesys_repark_sensor_after_white_shading(dev);
    } catch (...) {
        catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); });
        throw;
    }

    // in case we haven't black shading data, build it from black pixels of white calibration
    // FIXME: shouldn't we use genesys_dark_shading_by_dummy_pixel() ?
    if (!has_flag(dev->model->flags, ModelFlag::DARK_CALIBRATION)) {
        genesys_dark_shading_by_constant(*dev);
    }

  /* send the shading coefficient when doing whole line shading
   * but not when using SHDAREA like GL124 */
    if (!dev->cmd_set->has_send_shading_data()) {
        genesys_send_shading_coefficient(dev, sensor);
    }

    // save the calibration data
    genesys_save_calibration(dev, sensor);

    // and finally eject calibration sheet
    dev->cmd_set->eject_document(dev);

    // restore settings
    dev->settings.xres = sensor.full_resolution;
}

/**
 * does the calibration process for a device
 * @param dev device to calibrate
 */
static void genesys_scanner_calibration(Genesys_Device* dev, Genesys_Sensor& sensor)
{
    DBG_HELPER(dbg);
    if (!dev->model->is_sheetfed) {
        genesys_flatbed_calibration(dev, sensor);
        return;
    }
    genesys_sheetfed_calibration(dev, sensor);
}


/* ------------------------------------------------------------------------ */
/*                  High level (exported) functions                         */
/* ------------------------------------------------------------------------ */

/*
 * wait lamp to be warm enough by scanning the same line until
 * differences between two scans are below a threshold
 */
static void genesys_warmup_lamp(Genesys_Device* dev)
{
    DBG_HELPER(dbg);
    unsigned seconds = 0;

  const auto& sensor = sanei_genesys_find_sensor_any(dev);

    dev->cmd_set->init_regs_for_warmup(dev, sensor, &dev->reg);
    dev->interface->write_registers(dev->reg);

    auto total_pixels =  dev->session.output_pixels;
    auto total_size = dev->session.output_line_bytes;
    auto channels = dev->session.params.channels;
    auto lines = dev->session.output_line_count;

  std::vector<uint8_t> first_line(total_size);
  std::vector<uint8_t> second_line(total_size);

    do {
        first_line = second_line;

        dev->cmd_set->begin_scan(dev, sensor, &dev->reg, false);

        if (is_testing_mode()) {
            dev->interface->test_checkpoint("warmup_lamp");
            dev->cmd_set->end_scan(dev, &dev->reg, true);
            return;
        }

        wait_until_buffer_non_empty(dev);

        sanei_genesys_read_data_from_scanner(dev, second_line.data(), total_size);
        dev->cmd_set->end_scan(dev, &dev->reg, true);

        // compute difference between the two scans
        double first_average = 0;
        double second_average = 0;
        for (unsigned pixel = 0; pixel < total_size; pixel++) {
            // 16 bit data
            if (dev->session.params.depth == 16) {
                first_average += (first_line[pixel] + first_line[pixel + 1] * 256);
                second_average += (second_line[pixel] + second_line[pixel + 1] * 256);
                pixel++;
            } else {
                first_average += first_line[pixel];
                second_average += second_line[pixel];
            }
        }

        first_average /= total_pixels;
        second_average /= total_pixels;

        if (dbg_log_image_data()) {
            write_tiff_file("gl_warmup1.tiff", first_line.data(), dev->session.params.depth,
                            channels, total_size / (lines * channels), lines);
            write_tiff_file("gl_warmup2.tiff", second_line.data(), dev->session.params.depth,
                            channels, total_size / (lines * channels), lines);
        }

        DBG(DBG_info, "%s: average 1 = %.2f, average 2 = %.2f\n", __func__, first_average,
            second_average);

        float average_difference = std::fabs(first_average - second_average) / second_average;
        if (second_average > 0 && average_difference < 0.005)
        {
            dbg.vlog(DBG_info, "difference: %f, exiting", average_difference);
            break;
        }

        dev->interface->sleep_ms(1000);
        seconds++;
    } while (seconds < WARMUP_TIME);

  if (seconds >= WARMUP_TIME)
    {
        throw SaneException(SANE_STATUS_IO_ERROR,
                            "warmup timed out after %d seconds. Lamp defective?", seconds);
    }
  else
    {
      DBG(DBG_info, "%s: warmup succeeded after %d seconds\n", __func__, seconds);
    }
}

static void init_regs_for_scan(Genesys_Device& dev, const Genesys_Sensor& sensor,
                               Genesys_Register_Set& regs)
{
    DBG_HELPER(dbg);
    debug_dump(DBG_info, dev.settings);

    auto session = dev.cmd_set->calculate_scan_session(&dev, sensor, dev.settings);

    if (dev.model->asic_type == AsicType::GL124 ||
        dev.model->asic_type == AsicType::GL845 ||
        dev.model->asic_type == AsicType::GL846 ||
        dev.model->asic_type == AsicType::GL847)
    {
        /*  Fast move to scan area:

            We don't move fast the whole distance since it would involve computing
            acceleration/deceleration distance for scan resolution. So leave a remainder for it so
            scan makes the final move tuning
        */

        if (dev.settings.get_channels() * dev.settings.yres >= 600 && session.params.starty > 700) {
            scanner_move(dev, dev.model->default_method,
                         static_cast<unsigned>(session.params.starty - 500),
                         Direction::FORWARD);
            session.params.starty = 500;
        }
        compute_session(&dev, session, sensor);
    }

    dev.cmd_set->init_regs_for_scan_session(&dev, sensor, &regs, session);
}

// High-level start of scanning
static void genesys_start_scan(Genesys_Device* dev, bool lamp_off)
{
    DBG_HELPER(dbg);
  unsigned int steps, expected;


  /* since not all scanners are set to wait for head to park
   * we check we are not still parking before starting a new scan */
    if (dev->parking) {
        sanei_genesys_wait_for_home(dev);
    }

    // disable power saving
    dev->cmd_set->save_power(dev, false);

  /* wait for lamp warmup : until a warmup for TRANSPARENCY is designed, skip
   * it when scanning from XPA. */
    if (has_flag(dev->model->flags, ModelFlag::WARMUP) &&
        (dev->settings.scan_method != ScanMethod::TRANSPARENCY_INFRARED))
    {
        if (dev->settings.scan_method == ScanMethod::TRANSPARENCY ||
            dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED)
        {
            scanner_move_to_ta(*dev);
        }

        genesys_warmup_lamp(dev);
    }

  /* set top left x and y values by scanning the internals if flatbed scanners */
    if (!dev->model->is_sheetfed) {
        // TODO: check we can drop this since we cannot have the scanner's head wandering here
        dev->parking = false;
        dev->cmd_set->move_back_home(dev, true);
    }

  /* move to calibration area for transparency adapter */
    if (dev->settings.scan_method == ScanMethod::TRANSPARENCY ||
        dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED)
    {
        scanner_move_to_ta(*dev);
    }

  /* load document if needed (for sheetfed scanner for instance) */
    if (dev->model->is_sheetfed) {
        dev->cmd_set->load_document(dev);
    }

    auto& sensor = sanei_genesys_find_sensor_for_write(dev, dev->settings.xres,
                                                       dev->settings.get_channels(),
                                                       dev->settings.scan_method);

    // send gamma tables. They have been set to device or user value
    // when setting option value */
    dev->cmd_set->send_gamma_table(dev, sensor);

  /* try to use cached calibration first */
  if (!genesys_restore_calibration (dev, sensor))
    {
        // calibration : sheetfed scanners can't calibrate before each scan.
        // also don't run calibration for those scanners where all passes are disabled
        bool shading_disabled =
                has_flag(dev->model->flags, ModelFlag::DISABLE_ADC_CALIBRATION) &&
                has_flag(dev->model->flags, ModelFlag::DISABLE_EXPOSURE_CALIBRATION) &&
                has_flag(dev->model->flags, ModelFlag::DISABLE_SHADING_CALIBRATION);
        if (!shading_disabled && !dev->model->is_sheetfed) {
            genesys_scanner_calibration(dev, sensor);
            genesys_save_calibration(dev, sensor);
        } else {
          DBG(DBG_warn, "%s: no calibration done\n", __func__);
        }
    }

    dev->cmd_set->wait_for_motor_stop(dev);

    if (dev->cmd_set->needs_home_before_init_regs_for_scan(dev)) {
        dev->cmd_set->move_back_home(dev, true);
    }

    if (dev->settings.scan_method == ScanMethod::TRANSPARENCY ||
        dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED)
    {
        scanner_move_to_ta(*dev);
    }

    init_regs_for_scan(*dev, sensor, dev->reg);

  /* no lamp during scan */
    if (lamp_off) {
        sanei_genesys_set_lamp_power(dev, sensor, dev->reg, false);
    }

  /* GL124 is using SHDAREA, so we have to wait for scan to be set up before
   * sending shading data */
    if (dev->cmd_set->has_send_shading_data() &&
        !has_flag(dev->model->flags, ModelFlag::DISABLE_SHADING_CALIBRATION))
    {
        genesys_send_shading_coefficient(dev, sensor);
    }

    // now send registers for scan
    dev->interface->write_registers(dev->reg);

    // start effective scan
    dev->cmd_set->begin_scan(dev, sensor, &dev->reg, true);

    if (is_testing_mode()) {
        dev->interface->test_checkpoint("start_scan");
        return;
    }

  /*do we really need this? the valid data check should be sufficient -- pierre*/
  /* waits for head to reach scanning position */
  expected = dev->reg.get8(0x3d) * 65536
           + dev->reg.get8(0x3e) * 256
           + dev->reg.get8(0x3f);
  do
    {
        // wait some time between each test to avoid overloading USB and CPU
        dev->interface->sleep_ms(100);
        sanei_genesys_read_feed_steps (dev, &steps);
    }
  while (steps < expected);

    wait_until_buffer_non_empty(dev);

    // we wait for at least one word of valid scan data
    // this is also done in sanei_genesys_read_data_from_scanner -- pierre
    if (!dev->model->is_sheetfed) {
        do {
            dev->interface->sleep_ms(100);
            sanei_genesys_read_valid_words(dev, &steps);
        }
      while (steps < 1);
    }
}

/* this function does the effective data read in a manner that suits
   the scanner. It does data reordering and resizing if need.
   It also manages EOF and I/O errors, and line distance correction.
    Returns true on success, false on end-of-file.
*/
static void genesys_read_ordered_data(Genesys_Device* dev, SANE_Byte* destination, size_t* len)
{
    DBG_HELPER(dbg);
    size_t bytes = 0;

    if (!dev->read_active) {
      *len = 0;
        throw SaneException("read is not active");
    }

    DBG(DBG_info, "%s: frontend requested %zu bytes\n", __func__, *len);
    DBG(DBG_info, "%s: bytes_to_read=%zu, total_bytes_read=%zu\n", __func__,
        dev->total_bytes_to_read, dev->total_bytes_read);

  /* is there data left to scan */
  if (dev->total_bytes_read >= dev->total_bytes_to_read)
    {
      /* issue park command immediately in case scanner can handle it
       * so we save time */
        if (!dev->model->is_sheetfed && !has_flag(dev->model->flags, ModelFlag::MUST_WAIT) &&
            !dev->parking)
        {
            dev->cmd_set->move_back_home(dev, false);
            dev->parking = true;
        }
        throw SaneException(SANE_STATUS_EOF, "nothing more to scan: EOF");
    }

    if (is_testing_mode()) {
        if (dev->total_bytes_read + *len > dev->total_bytes_to_read) {
            *len = dev->total_bytes_to_read - dev->total_bytes_read;
        }
        dev->total_bytes_read += *len;
    } else {
        if (dev->model->is_sheetfed) {
            dev->cmd_set->detect_document_end(dev);
        }

        if (dev->total_bytes_read + *len > dev->total_bytes_to_read) {
            *len = dev->total_bytes_to_read - dev->total_bytes_read;
        }

        dev->pipeline_buffer.get_data(*len, destination);
        dev->total_bytes_read += *len;
    }

  /* end scan if all needed data have been read */
   if(dev->total_bytes_read >= dev->total_bytes_to_read)
    {
        dev->cmd_set->end_scan(dev, &dev->reg, true);
        if (dev->model->is_sheetfed) {
            dev->cmd_set->eject_document (dev);
        }
    }

    DBG(DBG_proc, "%s: completed, %zu bytes read\n", __func__, bytes);
}



/* ------------------------------------------------------------------------ */
/*                  Start of higher level functions                         */
/* ------------------------------------------------------------------------ */

static size_t
max_string_size (const SANE_String_Const strings[])
{
  size_t size, max_size = 0;
  SANE_Int i;

  for (i = 0; strings[i]; ++i)
    {
      size = strlen (strings[i]) + 1;
      if (size > max_size)
	max_size = size;
    }
  return max_size;
}

static std::size_t max_string_size(const std::vector<const char*>& strings)
{
    std::size_t max_size = 0;
    for (const auto& s : strings) {
        if (!s) {
            continue;
        }
        max_size = std::max(max_size, std::strlen(s));
    }
    return max_size;
}

static unsigned pick_resolution(const std::vector<unsigned>& resolutions, unsigned resolution,
                                const char* direction)
{
    DBG_HELPER(dbg);

    if (resolutions.empty())
        throw SaneException("Empty resolution list");

    unsigned best_res = resolutions.front();
    unsigned min_diff = abs_diff(best_res, resolution);

    for (auto it = std::next(resolutions.begin()); it != resolutions.end(); ++it) {
        unsigned curr_diff = abs_diff(*it, resolution);
        if (curr_diff < min_diff) {
            min_diff = curr_diff;
            best_res = *it;
        }
    }

    if (best_res != resolution) {
        DBG(DBG_warn, "%s: using resolution %d that is nearest to %d for direction %s\n",
            __func__, best_res, resolution, direction);
    }
    return best_res;
}

static Genesys_Settings calculate_scan_settings(Genesys_Scanner* s)
{
    DBG_HELPER(dbg);

    const auto* dev = s->dev;
    Genesys_Settings settings;
    settings.scan_method = s->scan_method;
    settings.scan_mode = option_string_to_scan_color_mode(s->mode);

    settings.depth = s->bit_depth;

    if (settings.depth > 8) {
        settings.depth = 16;
    } else if (settings.depth < 8) {
        settings.depth = 1;
    }

    const auto& resolutions = dev->model->get_resolution_settings(settings.scan_method);

    settings.xres = pick_resolution(resolutions.resolutions_x, s->resolution, "X");
    settings.yres = pick_resolution(resolutions.resolutions_y, s->resolution, "Y");

    settings.tl_x = fixed_to_float(s->pos_top_left_x);
    settings.tl_y = fixed_to_float(s->pos_top_left_y);
    float br_x = fixed_to_float(s->pos_bottom_right_x);
    float br_y = fixed_to_float(s->pos_bottom_right_y);

    settings.lines = static_cast<unsigned>(((br_y - settings.tl_y) * settings.yres) /
                                            MM_PER_INCH);


    unsigned pixels_per_line = static_cast<unsigned>(((br_x - settings.tl_x) * settings.xres) /
                                                     MM_PER_INCH);

    const auto& sensor = sanei_genesys_find_sensor(dev, settings.xres, settings.get_channels(),
                                                   settings.scan_method);

    pixels_per_line = session_adjust_output_pixels(pixels_per_line, *dev, sensor,
                                                   settings.xres, settings.yres, true);

    unsigned xres_factor = s->resolution / settings.xres;
    settings.pixels = pixels_per_line;
    settings.requested_pixels = pixels_per_line * xres_factor;

    if (s->color_filter == "Red") {
        settings.color_filter = ColorFilter::RED;
    } else if (s->color_filter == "Green") {
        settings.color_filter = ColorFilter::GREEN;
    } else if (s->color_filter == "Blue") {
        settings.color_filter = ColorFilter::BLUE;
    } else {
        settings.color_filter = ColorFilter::NONE;
    }

    // brightness and contrast only for for 8 bit scans
    if (s->bit_depth == 8) {
        settings.contrast = (s->contrast * 127) / 100;
        settings.brightness = (s->brightness * 127) / 100;
    } else {
        settings.contrast = 0;
        settings.brightness = 0;
    }

    settings.expiration_time = s->expiration_time;

    return settings;
}

static SANE_Parameters calculate_scan_parameters(const Genesys_Device& dev,
                                                 const Genesys_Settings& settings)
{
    DBG_HELPER(dbg);

    auto sensor = sanei_genesys_find_sensor(&dev, settings.xres, settings.get_channels(),
                                            settings.scan_method);
    auto session = dev.cmd_set->calculate_scan_session(&dev, sensor, settings);
    auto pipeline = build_image_pipeline(dev, session, 0, false);

    SANE_Parameters params;
    if (settings.scan_mode == ScanColorMode::GRAY) {
        params.format = SANE_FRAME_GRAY;
    } else {
        params.format = SANE_FRAME_RGB;
    }
    // only single-pass scanning supported
    params.last_frame = true;
    params.depth = settings.depth;
    params.lines = pipeline.get_output_height();
    params.pixels_per_line = pipeline.get_output_width();
    params.bytes_per_line = pipeline.get_output_row_bytes();

    return params;
}

static void calc_parameters(Genesys_Scanner* s)
{
    DBG_HELPER(dbg);

    s->dev->settings = calculate_scan_settings(s);
    s->params = calculate_scan_parameters(*s->dev, s->dev->settings);
}

static void create_bpp_list (Genesys_Scanner * s, const std::vector<unsigned>& bpp)
{
    s->bpp_list[0] = bpp.size();
    std::reverse_copy(bpp.begin(), bpp.end(), s->bpp_list + 1);
}

/** @brief this function initialize a gamma vector based on the ASIC:
 * Set up a default gamma table vector based on device description
 * gl646: 12 or 14 bits gamma table depending on ModelFlag::GAMMA_14BIT
 * gl84x: 16 bits
 * gl12x: 16 bits
 * @param scanner pointer to scanner session to get options
 * @param option option number of the gamma table to set
 */
static void
init_gamma_vector_option (Genesys_Scanner * scanner, int option)
{
  /* the option is inactive until the custom gamma control
   * is enabled */
  scanner->opt[option].type = SANE_TYPE_INT;
  scanner->opt[option].cap |= SANE_CAP_INACTIVE | SANE_CAP_ADVANCED;
  scanner->opt[option].unit = SANE_UNIT_NONE;
  scanner->opt[option].constraint_type = SANE_CONSTRAINT_RANGE;
    if (scanner->dev->model->asic_type == AsicType::GL646) {
        if (has_flag(scanner->dev->model->flags, ModelFlag::GAMMA_14BIT)) {
	  scanner->opt[option].size = 16384 * sizeof (SANE_Word);
	  scanner->opt[option].constraint.range = &u14_range;
	}
      else
	{			/* 12 bits gamma tables */
	  scanner->opt[option].size = 4096 * sizeof (SANE_Word);
	  scanner->opt[option].constraint.range = &u12_range;
	}
    }
  else
    {				/* other asics have 16 bits words gamma table */
      scanner->opt[option].size = 256 * sizeof (SANE_Word);
      scanner->opt[option].constraint.range = &u16_range;
    }
}

/**
 * allocate a geometry range
 * @param size maximum size of the range
 * @return a pointer to a valid range or nullptr
 */
static SANE_Range create_range(float size)
{
    SANE_Range range;
    range.min = float_to_fixed(0.0);
    range.max = float_to_fixed(size);
    range.quant = float_to_fixed(0.0);
    return range;
}

/** @brief generate calibration cache file nam
 * Generates the calibration cache file name to use.
 * Tries to store the cache in $HOME/.sane or
 * then fallbacks to $TMPDIR or TMP. The filename
 * uses the model name if only one scanner is plugged
 * else is uses the device name when several identical
 * scanners are in use.
 * @param currdev current scanner device
 * @return an allocated string containing a file name
 */
static std::string calibration_filename(Genesys_Device *currdev)
{
    std::string ret;
    ret.resize(PATH_MAX);

  char filename[80];
  unsigned int count;
  unsigned int i;

  /* first compute the DIR where we can store cache:
   * 1 - home dir
   * 2 - $TMPDIR
   * 3 - $TMP
   * 4 - tmp dir
   * 5 - temp dir
   * 6 - then resort to current dir
   */
    char* ptr = std::getenv("HOME");
    if (ptr == nullptr) {
        ptr = std::getenv("USERPROFILE");
    }
    if (ptr == nullptr) {
        ptr = std::getenv("TMPDIR");
    }
    if (ptr == nullptr) {
        ptr = std::getenv("TMP");
    }

  /* now choose filename:
   * 1 - if only one scanner, name of the model
   * 2 - if several scanners of the same model, use device name,
   *     replacing special chars
   */
  count=0;
  /* count models of the same names if several scanners attached */
    if(s_devices->size() > 1) {
        for (const auto& dev : *s_devices) {
            if (dev.vendorId == currdev->vendorId && dev.productId == currdev->productId) {
                count++;
            }
        }
    }
  if(count>1)
    {
        std::snprintf(filename, sizeof(filename), "%s.cal", currdev->file_name.c_str());
      for(i=0;i<strlen(filename);i++)
        {
          if(filename[i]==':'||filename[i]==PATH_SEP)
            {
              filename[i]='_';
            }
        }
    }
  else
    {
      snprintf(filename,sizeof(filename),"%s.cal",currdev->model->name);
    }

  /* build final final name : store dir + filename */
    if (ptr == nullptr) {
        int size = std::snprintf(&ret.front(), ret.size(), "%s", filename);
        ret.resize(size);
    }
  else
    {
        int size = 0;
#ifdef HAVE_MKDIR
        /* make sure .sane directory exists in existing store dir */
        size = std::snprintf(&ret.front(), ret.size(), "%s%c.sane", ptr, PATH_SEP);
        ret.resize(size);
        mkdir(ret.c_str(), 0700);

        ret.resize(PATH_MAX);
#endif
        size = std::snprintf(&ret.front(), ret.size(), "%s%c.sane%c%s",
                             ptr, PATH_SEP, PATH_SEP, filename);
        ret.resize(size);
    }

    DBG(DBG_info, "%s: calibration filename >%s<\n", __func__, ret.c_str());

    return ret;
}

static void set_resolution_option_values(Genesys_Scanner& s, bool reset_resolution_value)
{
    auto resolutions = s.dev->model->get_resolutions(s.scan_method);

    s.opt_resolution_values.resize(resolutions.size() + 1, 0);
    s.opt_resolution_values[0] = resolutions.size();
    std::copy(resolutions.begin(), resolutions.end(), s.opt_resolution_values.begin() + 1);

    s.opt[OPT_RESOLUTION].constraint.word_list = s.opt_resolution_values.data();

    if (reset_resolution_value) {
        s.resolution = *std::min_element(resolutions.begin(), resolutions.end());
    }
}

static void set_xy_range_option_values(Genesys_Scanner& s)
{
    if (s.scan_method == ScanMethod::FLATBED)
    {
        s.opt_x_range = create_range(s.dev->model->x_size);
        s.opt_y_range = create_range(s.dev->model->y_size);
    }
  else
    {
        s.opt_x_range = create_range(s.dev->model->x_size_ta);
        s.opt_y_range = create_range(s.dev->model->y_size_ta);
    }

    s.opt[OPT_TL_X].constraint.range = &s.opt_x_range;
    s.opt[OPT_TL_Y].constraint.range = &s.opt_y_range;
    s.opt[OPT_BR_X].constraint.range = &s.opt_x_range;
    s.opt[OPT_BR_Y].constraint.range = &s.opt_y_range;

    s.pos_top_left_x = 0;
    s.pos_top_left_y = 0;
    s.pos_bottom_right_x = s.opt_x_range.max;
    s.pos_bottom_right_y = s.opt_y_range.max;
}

static void init_options(Genesys_Scanner* s)
{
    DBG_HELPER(dbg);
  SANE_Int option;
    const Genesys_Model* model = s->dev->model;

  memset (s->opt, 0, sizeof (s->opt));

  for (option = 0; option < NUM_OPTIONS; ++option)
    {
      s->opt[option].size = sizeof (SANE_Word);
      s->opt[option].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT;
    }
  s->opt[OPT_NUM_OPTS].name = SANE_NAME_NUM_OPTIONS;
  s->opt[OPT_NUM_OPTS].title = SANE_TITLE_NUM_OPTIONS;
  s->opt[OPT_NUM_OPTS].desc = SANE_DESC_NUM_OPTIONS;
  s->opt[OPT_NUM_OPTS].type = SANE_TYPE_INT;
  s->opt[OPT_NUM_OPTS].cap = SANE_CAP_SOFT_DETECT;

  /* "Mode" group: */
  s->opt[OPT_MODE_GROUP].name = "scanmode-group";
  s->opt[OPT_MODE_GROUP].title = SANE_I18N ("Scan Mode");
  s->opt[OPT_MODE_GROUP].desc = "";
  s->opt[OPT_MODE_GROUP].type = SANE_TYPE_GROUP;
  s->opt[OPT_MODE_GROUP].size = 0;
  s->opt[OPT_MODE_GROUP].cap = 0;
  s->opt[OPT_MODE_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

  /* scan mode */
  s->opt[OPT_MODE].name = SANE_NAME_SCAN_MODE;
  s->opt[OPT_MODE].title = SANE_TITLE_SCAN_MODE;
  s->opt[OPT_MODE].desc = SANE_DESC_SCAN_MODE;
  s->opt[OPT_MODE].type = SANE_TYPE_STRING;
  s->opt[OPT_MODE].constraint_type = SANE_CONSTRAINT_STRING_LIST;
  s->opt[OPT_MODE].size = max_string_size (mode_list);
  s->opt[OPT_MODE].constraint.string_list = mode_list;
  s->mode = SANE_VALUE_SCAN_MODE_GRAY;

  /* scan source */
    s->opt_source_values.clear();
    for (const auto& resolution_setting : model->resolutions) {
        for (auto method : resolution_setting.methods) {
            s->opt_source_values.push_back(scan_method_to_option_string(method));
        }
    }
    s->opt_source_values.push_back(nullptr);

  s->opt[OPT_SOURCE].name = SANE_NAME_SCAN_SOURCE;
  s->opt[OPT_SOURCE].title = SANE_TITLE_SCAN_SOURCE;
  s->opt[OPT_SOURCE].desc = SANE_DESC_SCAN_SOURCE;
  s->opt[OPT_SOURCE].type = SANE_TYPE_STRING;
  s->opt[OPT_SOURCE].constraint_type = SANE_CONSTRAINT_STRING_LIST;
    s->opt[OPT_SOURCE].size = max_string_size(s->opt_source_values);
    s->opt[OPT_SOURCE].constraint.string_list = s->opt_source_values.data();
    if (s->opt_source_values.size() < 2) {
        throw SaneException("No scan methods specified for scanner");
    }
    s->scan_method = model->default_method;

  /* preview */
  s->opt[OPT_PREVIEW].name = SANE_NAME_PREVIEW;
  s->opt[OPT_PREVIEW].title = SANE_TITLE_PREVIEW;
  s->opt[OPT_PREVIEW].desc = SANE_DESC_PREVIEW;
  s->opt[OPT_PREVIEW].type = SANE_TYPE_BOOL;
  s->opt[OPT_PREVIEW].unit = SANE_UNIT_NONE;
  s->opt[OPT_PREVIEW].constraint_type = SANE_CONSTRAINT_NONE;
  s->preview = false;

  /* bit depth */
  s->opt[OPT_BIT_DEPTH].name = SANE_NAME_BIT_DEPTH;
  s->opt[OPT_BIT_DEPTH].title = SANE_TITLE_BIT_DEPTH;
  s->opt[OPT_BIT_DEPTH].desc = SANE_DESC_BIT_DEPTH;
  s->opt[OPT_BIT_DEPTH].type = SANE_TYPE_INT;
  s->opt[OPT_BIT_DEPTH].constraint_type = SANE_CONSTRAINT_WORD_LIST;
  s->opt[OPT_BIT_DEPTH].size = sizeof (SANE_Word);
  s->opt[OPT_BIT_DEPTH].constraint.word_list = s->bpp_list;
  create_bpp_list (s, model->bpp_gray_values);
    s->bit_depth = model->bpp_gray_values[0];

    // resolution
  s->opt[OPT_RESOLUTION].name = SANE_NAME_SCAN_RESOLUTION;
  s->opt[OPT_RESOLUTION].title = SANE_TITLE_SCAN_RESOLUTION;
  s->opt[OPT_RESOLUTION].desc = SANE_DESC_SCAN_RESOLUTION;
  s->opt[OPT_RESOLUTION].type = SANE_TYPE_INT;
  s->opt[OPT_RESOLUTION].unit = SANE_UNIT_DPI;
  s->opt[OPT_RESOLUTION].constraint_type = SANE_CONSTRAINT_WORD_LIST;
    set_resolution_option_values(*s, true);

  /* "Geometry" group: */
  s->opt[OPT_GEOMETRY_GROUP].name = SANE_NAME_GEOMETRY;
  s->opt[OPT_GEOMETRY_GROUP].title = SANE_I18N ("Geometry");
  s->opt[OPT_GEOMETRY_GROUP].desc = "";
  s->opt[OPT_GEOMETRY_GROUP].type = SANE_TYPE_GROUP;
  s->opt[OPT_GEOMETRY_GROUP].cap = SANE_CAP_ADVANCED;
  s->opt[OPT_GEOMETRY_GROUP].size = 0;
  s->opt[OPT_GEOMETRY_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

    s->opt_x_range = create_range(model->x_size);
    s->opt_y_range = create_range(model->y_size);

    // scan area
  s->opt[OPT_TL_X].name = SANE_NAME_SCAN_TL_X;
  s->opt[OPT_TL_X].title = SANE_TITLE_SCAN_TL_X;
  s->opt[OPT_TL_X].desc = SANE_DESC_SCAN_TL_X;
  s->opt[OPT_TL_X].type = SANE_TYPE_FIXED;
  s->opt[OPT_TL_X].unit = SANE_UNIT_MM;
  s->opt[OPT_TL_X].constraint_type = SANE_CONSTRAINT_RANGE;

  s->opt[OPT_TL_Y].name = SANE_NAME_SCAN_TL_Y;
  s->opt[OPT_TL_Y].title = SANE_TITLE_SCAN_TL_Y;
  s->opt[OPT_TL_Y].desc = SANE_DESC_SCAN_TL_Y;
  s->opt[OPT_TL_Y].type = SANE_TYPE_FIXED;
  s->opt[OPT_TL_Y].unit = SANE_UNIT_MM;
  s->opt[OPT_TL_Y].constraint_type = SANE_CONSTRAINT_RANGE;

  s->opt[OPT_BR_X].name = SANE_NAME_SCAN_BR_X;
  s->opt[OPT_BR_X].title = SANE_TITLE_SCAN_BR_X;
  s->opt[OPT_BR_X].desc = SANE_DESC_SCAN_BR_X;
  s->opt[OPT_BR_X].type = SANE_TYPE_FIXED;
  s->opt[OPT_BR_X].unit = SANE_UNIT_MM;
  s->opt[OPT_BR_X].constraint_type = SANE_CONSTRAINT_RANGE;

  s->opt[OPT_BR_Y].name = SANE_NAME_SCAN_BR_Y;
  s->opt[OPT_BR_Y].title = SANE_TITLE_SCAN_BR_Y;
  s->opt[OPT_BR_Y].desc = SANE_DESC_SCAN_BR_Y;
  s->opt[OPT_BR_Y].type = SANE_TYPE_FIXED;
  s->opt[OPT_BR_Y].unit = SANE_UNIT_MM;
  s->opt[OPT_BR_Y].constraint_type = SANE_CONSTRAINT_RANGE;

    set_xy_range_option_values(*s);

  /* "Enhancement" group: */
  s->opt[OPT_ENHANCEMENT_GROUP].name = SANE_NAME_ENHANCEMENT;
  s->opt[OPT_ENHANCEMENT_GROUP].title = SANE_I18N ("Enhancement");
  s->opt[OPT_ENHANCEMENT_GROUP].desc = "";
  s->opt[OPT_ENHANCEMENT_GROUP].type = SANE_TYPE_GROUP;
  s->opt[OPT_ENHANCEMENT_GROUP].cap = SANE_CAP_ADVANCED;
  s->opt[OPT_ENHANCEMENT_GROUP].size = 0;
  s->opt[OPT_ENHANCEMENT_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

  /* custom-gamma table */
  s->opt[OPT_CUSTOM_GAMMA].name = SANE_NAME_CUSTOM_GAMMA;
  s->opt[OPT_CUSTOM_GAMMA].title = SANE_TITLE_CUSTOM_GAMMA;
  s->opt[OPT_CUSTOM_GAMMA].desc = SANE_DESC_CUSTOM_GAMMA;
  s->opt[OPT_CUSTOM_GAMMA].type = SANE_TYPE_BOOL;
  s->opt[OPT_CUSTOM_GAMMA].cap |= SANE_CAP_ADVANCED;
  s->custom_gamma = false;

  /* grayscale gamma vector */
  s->opt[OPT_GAMMA_VECTOR].name = SANE_NAME_GAMMA_VECTOR;
  s->opt[OPT_GAMMA_VECTOR].title = SANE_TITLE_GAMMA_VECTOR;
  s->opt[OPT_GAMMA_VECTOR].desc = SANE_DESC_GAMMA_VECTOR;
  init_gamma_vector_option (s, OPT_GAMMA_VECTOR);

  /* red gamma vector */
  s->opt[OPT_GAMMA_VECTOR_R].name = SANE_NAME_GAMMA_VECTOR_R;
  s->opt[OPT_GAMMA_VECTOR_R].title = SANE_TITLE_GAMMA_VECTOR_R;
  s->opt[OPT_GAMMA_VECTOR_R].desc = SANE_DESC_GAMMA_VECTOR_R;
  init_gamma_vector_option (s, OPT_GAMMA_VECTOR_R);

  /* green gamma vector */
  s->opt[OPT_GAMMA_VECTOR_G].name = SANE_NAME_GAMMA_VECTOR_G;
  s->opt[OPT_GAMMA_VECTOR_G].title = SANE_TITLE_GAMMA_VECTOR_G;
  s->opt[OPT_GAMMA_VECTOR_G].desc = SANE_DESC_GAMMA_VECTOR_G;
  init_gamma_vector_option (s, OPT_GAMMA_VECTOR_G);

  /* blue gamma vector */
  s->opt[OPT_GAMMA_VECTOR_B].name = SANE_NAME_GAMMA_VECTOR_B;
  s->opt[OPT_GAMMA_VECTOR_B].title = SANE_TITLE_GAMMA_VECTOR_B;
  s->opt[OPT_GAMMA_VECTOR_B].desc = SANE_DESC_GAMMA_VECTOR_B;
  init_gamma_vector_option (s, OPT_GAMMA_VECTOR_B);

  /* currently, there are only gamma table options in this group,
   * so if the scanner doesn't support gamma table, disable the
   * whole group */
    if (!has_flag(model->flags, ModelFlag::CUSTOM_GAMMA)) {
      s->opt[OPT_ENHANCEMENT_GROUP].cap |= SANE_CAP_INACTIVE;
      s->opt[OPT_CUSTOM_GAMMA].cap |= SANE_CAP_INACTIVE;
      DBG(DBG_info, "%s: custom gamma disabled\n", __func__);
    }

  /* software base image enhancements, these are consuming as many
   * memory than used by the full scanned image and may fail at high
   * resolution
   */

  /* Software brightness */
  s->opt[OPT_BRIGHTNESS].name = SANE_NAME_BRIGHTNESS;
  s->opt[OPT_BRIGHTNESS].title = SANE_TITLE_BRIGHTNESS;
  s->opt[OPT_BRIGHTNESS].desc = SANE_DESC_BRIGHTNESS;
  s->opt[OPT_BRIGHTNESS].type = SANE_TYPE_INT;
  s->opt[OPT_BRIGHTNESS].unit = SANE_UNIT_NONE;
  s->opt[OPT_BRIGHTNESS].constraint_type = SANE_CONSTRAINT_RANGE;
  s->opt[OPT_BRIGHTNESS].constraint.range = &(enhance_range);
  s->opt[OPT_BRIGHTNESS].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT;
  s->brightness = 0;    // disable by default

  /* Sowftware contrast */
  s->opt[OPT_CONTRAST].name = SANE_NAME_CONTRAST;
  s->opt[OPT_CONTRAST].title = SANE_TITLE_CONTRAST;
  s->opt[OPT_CONTRAST].desc = SANE_DESC_CONTRAST;
  s->opt[OPT_CONTRAST].type = SANE_TYPE_INT;
  s->opt[OPT_CONTRAST].unit = SANE_UNIT_NONE;
  s->opt[OPT_CONTRAST].constraint_type = SANE_CONSTRAINT_RANGE;
  s->opt[OPT_CONTRAST].constraint.range = &(enhance_range);
  s->opt[OPT_CONTRAST].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT;
  s->contrast = 0;  // disable by default

  /* "Extras" group: */
  s->opt[OPT_EXTRAS_GROUP].name = "extras-group";
  s->opt[OPT_EXTRAS_GROUP].title = SANE_I18N ("Extras");
  s->opt[OPT_EXTRAS_GROUP].desc = "";
  s->opt[OPT_EXTRAS_GROUP].type = SANE_TYPE_GROUP;
  s->opt[OPT_EXTRAS_GROUP].cap = SANE_CAP_ADVANCED;
  s->opt[OPT_EXTRAS_GROUP].size = 0;
  s->opt[OPT_EXTRAS_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

  /* color filter */
  s->opt[OPT_COLOR_FILTER].name = "color-filter";
  s->opt[OPT_COLOR_FILTER].title = SANE_I18N ("Color filter");
  s->opt[OPT_COLOR_FILTER].desc =
    SANE_I18N
    ("When using gray or lineart this option selects the used color.");
  s->opt[OPT_COLOR_FILTER].type = SANE_TYPE_STRING;
  s->opt[OPT_COLOR_FILTER].constraint_type = SANE_CONSTRAINT_STRING_LIST;
  /* true gray not yet supported for GL847 and GL124 scanners */
    if (!model->is_cis || model->asic_type==AsicType::GL847 || model->asic_type==AsicType::GL124) {
      s->opt[OPT_COLOR_FILTER].size = max_string_size (color_filter_list);
      s->opt[OPT_COLOR_FILTER].constraint.string_list = color_filter_list;
      s->color_filter = s->opt[OPT_COLOR_FILTER].constraint.string_list[1];
    }
  else
    {
      s->opt[OPT_COLOR_FILTER].size = max_string_size (cis_color_filter_list);
      s->opt[OPT_COLOR_FILTER].constraint.string_list = cis_color_filter_list;
      /* default to "None" ie true gray */
      s->color_filter = s->opt[OPT_COLOR_FILTER].constraint.string_list[3];
    }

    // no support for color filter for cis+gl646 scanners
    if (model->asic_type == AsicType::GL646 && model->is_cis) {
      DISABLE (OPT_COLOR_FILTER);
    }

  /* calibration store file name */
  s->opt[OPT_CALIBRATION_FILE].name = "calibration-file";
  s->opt[OPT_CALIBRATION_FILE].title = SANE_I18N ("Calibration file");
  s->opt[OPT_CALIBRATION_FILE].desc = SANE_I18N ("Specify the calibration file to use");
  s->opt[OPT_CALIBRATION_FILE].type = SANE_TYPE_STRING;
  s->opt[OPT_CALIBRATION_FILE].unit = SANE_UNIT_NONE;
  s->opt[OPT_CALIBRATION_FILE].size = PATH_MAX;
  s->opt[OPT_CALIBRATION_FILE].cap = SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT | SANE_CAP_ADVANCED;
  s->opt[OPT_CALIBRATION_FILE].constraint_type = SANE_CONSTRAINT_NONE;
  s->calibration_file.clear();
  /* disable option if run as root */
#ifdef HAVE_GETUID
  if(geteuid()==0)
    {
      DISABLE (OPT_CALIBRATION_FILE);
    }
#endif

  /* expiration time for calibration cache entries */
  s->opt[OPT_EXPIRATION_TIME].name = "expiration-time";
  s->opt[OPT_EXPIRATION_TIME].title = SANE_I18N ("Calibration cache expiration time");
  s->opt[OPT_EXPIRATION_TIME].desc = SANE_I18N ("Time (in minutes) before a cached calibration expires. "
     "A value of 0 means cache is not used. A negative value means cache never expires.");
  s->opt[OPT_EXPIRATION_TIME].type = SANE_TYPE_INT;
  s->opt[OPT_EXPIRATION_TIME].unit = SANE_UNIT_NONE;
  s->opt[OPT_EXPIRATION_TIME].constraint_type = SANE_CONSTRAINT_RANGE;
  s->opt[OPT_EXPIRATION_TIME].constraint.range = &expiration_range;
  s->expiration_time = 60;  // 60 minutes by default

  /* Powersave time (turn lamp off) */
  s->opt[OPT_LAMP_OFF_TIME].name = "lamp-off-time";
  s->opt[OPT_LAMP_OFF_TIME].title = SANE_I18N ("Lamp off time");
  s->opt[OPT_LAMP_OFF_TIME].desc =
    SANE_I18N
    ("The lamp will be turned off after the given time (in minutes). "
     "A value of 0 means, that the lamp won't be turned off.");
  s->opt[OPT_LAMP_OFF_TIME].type = SANE_TYPE_INT;
  s->opt[OPT_LAMP_OFF_TIME].unit = SANE_UNIT_NONE;
  s->opt[OPT_LAMP_OFF_TIME].constraint_type = SANE_CONSTRAINT_RANGE;
  s->opt[OPT_LAMP_OFF_TIME].constraint.range = &time_range;
  s->lamp_off_time = 15;    // 15 minutes

  /* turn lamp off during scan */
  s->opt[OPT_LAMP_OFF].name = "lamp-off-scan";
  s->opt[OPT_LAMP_OFF].title = SANE_I18N ("Lamp off during scan");
  s->opt[OPT_LAMP_OFF].desc = SANE_I18N ("The lamp will be turned off during scan. ");
  s->opt[OPT_LAMP_OFF].type = SANE_TYPE_BOOL;
  s->opt[OPT_LAMP_OFF].unit = SANE_UNIT_NONE;
  s->opt[OPT_LAMP_OFF].constraint_type = SANE_CONSTRAINT_NONE;
  s->lamp_off = false;

  s->opt[OPT_SENSOR_GROUP].name = SANE_NAME_SENSORS;
  s->opt[OPT_SENSOR_GROUP].title = SANE_TITLE_SENSORS;
  s->opt[OPT_SENSOR_GROUP].desc = SANE_DESC_SENSORS;
  s->opt[OPT_SENSOR_GROUP].type = SANE_TYPE_GROUP;
  s->opt[OPT_SENSOR_GROUP].cap = SANE_CAP_ADVANCED;
  s->opt[OPT_SENSOR_GROUP].size = 0;
  s->opt[OPT_SENSOR_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

  s->opt[OPT_SCAN_SW].name = SANE_NAME_SCAN;
  s->opt[OPT_SCAN_SW].title = SANE_TITLE_SCAN;
  s->opt[OPT_SCAN_SW].desc = SANE_DESC_SCAN;
  s->opt[OPT_SCAN_SW].type = SANE_TYPE_BOOL;
  s->opt[OPT_SCAN_SW].unit = SANE_UNIT_NONE;
  if (model->buttons & GENESYS_HAS_SCAN_SW)
    s->opt[OPT_SCAN_SW].cap =
      SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
  else
    s->opt[OPT_SCAN_SW].cap = SANE_CAP_INACTIVE;

  /* SANE_NAME_FILE is not for buttons */
  s->opt[OPT_FILE_SW].name = "file";
  s->opt[OPT_FILE_SW].title = SANE_I18N ("File button");
  s->opt[OPT_FILE_SW].desc = SANE_I18N ("File button");
  s->opt[OPT_FILE_SW].type = SANE_TYPE_BOOL;
  s->opt[OPT_FILE_SW].unit = SANE_UNIT_NONE;
  if (model->buttons & GENESYS_HAS_FILE_SW)
    s->opt[OPT_FILE_SW].cap =
      SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
  else
    s->opt[OPT_FILE_SW].cap = SANE_CAP_INACTIVE;

  s->opt[OPT_EMAIL_SW].name = SANE_NAME_EMAIL;
  s->opt[OPT_EMAIL_SW].title = SANE_TITLE_EMAIL;
  s->opt[OPT_EMAIL_SW].desc = SANE_DESC_EMAIL;
  s->opt[OPT_EMAIL_SW].type = SANE_TYPE_BOOL;
  s->opt[OPT_EMAIL_SW].unit = SANE_UNIT_NONE;
  if (model->buttons & GENESYS_HAS_EMAIL_SW)
    s->opt[OPT_EMAIL_SW].cap =
      SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
  else
    s->opt[OPT_EMAIL_SW].cap = SANE_CAP_INACTIVE;

  s->opt[OPT_COPY_SW].name = SANE_NAME_COPY;
  s->opt[OPT_COPY_SW].title = SANE_TITLE_COPY;
  s->opt[OPT_COPY_SW].desc = SANE_DESC_COPY;
  s->opt[OPT_COPY_SW].type = SANE_TYPE_BOOL;
  s->opt[OPT_COPY_SW].unit = SANE_UNIT_NONE;
  if (model->buttons & GENESYS_HAS_COPY_SW)
    s->opt[OPT_COPY_SW].cap =
      SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
  else
    s->opt[OPT_COPY_SW].cap = SANE_CAP_INACTIVE;

  s->opt[OPT_PAGE_LOADED_SW].name = SANE_NAME_PAGE_LOADED;
  s->opt[OPT_PAGE_LOADED_SW].title = SANE_TITLE_PAGE_LOADED;
  s->opt[OPT_PAGE_LOADED_SW].desc = SANE_DESC_PAGE_LOADED;
  s->opt[OPT_PAGE_LOADED_SW].type = SANE_TYPE_BOOL;
  s->opt[OPT_PAGE_LOADED_SW].unit = SANE_UNIT_NONE;
  if (model->buttons & GENESYS_HAS_PAGE_LOADED_SW)
    s->opt[OPT_PAGE_LOADED_SW].cap =
      SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
  else
    s->opt[OPT_PAGE_LOADED_SW].cap = SANE_CAP_INACTIVE;

  /* OCR button */
  s->opt[OPT_OCR_SW].name = "ocr";
  s->opt[OPT_OCR_SW].title = SANE_I18N ("OCR button");
  s->opt[OPT_OCR_SW].desc = SANE_I18N ("OCR button");
  s->opt[OPT_OCR_SW].type = SANE_TYPE_BOOL;
  s->opt[OPT_OCR_SW].unit = SANE_UNIT_NONE;
  if (model->buttons & GENESYS_HAS_OCR_SW)
    s->opt[OPT_OCR_SW].cap =
      SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
  else
    s->opt[OPT_OCR_SW].cap = SANE_CAP_INACTIVE;

  /* power button */
  s->opt[OPT_POWER_SW].name = "power";
  s->opt[OPT_POWER_SW].title = SANE_I18N ("Power button");
  s->opt[OPT_POWER_SW].desc = SANE_I18N ("Power button");
  s->opt[OPT_POWER_SW].type = SANE_TYPE_BOOL;
  s->opt[OPT_POWER_SW].unit = SANE_UNIT_NONE;
  if (model->buttons & GENESYS_HAS_POWER_SW)
    s->opt[OPT_POWER_SW].cap =
      SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
  else
    s->opt[OPT_POWER_SW].cap = SANE_CAP_INACTIVE;

  /* extra button */
    s->opt[OPT_EXTRA_SW].name = "extra";
    s->opt[OPT_EXTRA_SW].title = SANE_I18N("Extra button");
    s->opt[OPT_EXTRA_SW].desc = SANE_I18N("Extra button");
    s->opt[OPT_EXTRA_SW].type = SANE_TYPE_BOOL;
    s->opt[OPT_EXTRA_SW].unit = SANE_UNIT_NONE;
    if (model->buttons & GENESYS_HAS_EXTRA_SW) {
        s->opt[OPT_EXTRA_SW].cap = SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
    } else {
        s->opt[OPT_EXTRA_SW].cap = SANE_CAP_INACTIVE;
    }

    // transparency/scan_film button
    s->opt[OPT_TRANSP_SW].name = "transparency";
    s->opt[OPT_TRANSP_SW].title = SANE_I18N ("Transparency button");
    s->opt[OPT_TRANSP_SW].desc = SANE_I18N ("Transparency button");
    s->opt[OPT_TRANSP_SW].type = SANE_TYPE_BOOL;
    s->opt[OPT_TRANSP_SW].unit = SANE_UNIT_NONE;
    if (model->buttons & GENESYS_HAS_TRANSP_SW) {
        s->opt[OPT_TRANSP_SW].cap = SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
    } else {
        s->opt[OPT_TRANSP_SW].cap = SANE_CAP_INACTIVE;
    }

  /* calibration needed */
  s->opt[OPT_NEED_CALIBRATION_SW].name = "need-calibration";
  s->opt[OPT_NEED_CALIBRATION_SW].title = SANE_I18N ("Needs calibration");
  s->opt[OPT_NEED_CALIBRATION_SW].desc = SANE_I18N ("The scanner needs calibration for the current settings");
  s->opt[OPT_NEED_CALIBRATION_SW].type = SANE_TYPE_BOOL;
  s->opt[OPT_NEED_CALIBRATION_SW].unit = SANE_UNIT_NONE;
  if (model->buttons & GENESYS_HAS_CALIBRATE)
    s->opt[OPT_NEED_CALIBRATION_SW].cap =
      SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED;
  else
    s->opt[OPT_NEED_CALIBRATION_SW].cap = SANE_CAP_INACTIVE;

  /* button group */
  s->opt[OPT_BUTTON_GROUP].name = "buttons";
  s->opt[OPT_BUTTON_GROUP].title = SANE_I18N ("Buttons");
  s->opt[OPT_BUTTON_GROUP].desc = "";
  s->opt[OPT_BUTTON_GROUP].type = SANE_TYPE_GROUP;
  s->opt[OPT_BUTTON_GROUP].cap = SANE_CAP_ADVANCED;
  s->opt[OPT_BUTTON_GROUP].size = 0;
  s->opt[OPT_BUTTON_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

  /* calibrate button */
  s->opt[OPT_CALIBRATE].name = "calibrate";
  s->opt[OPT_CALIBRATE].title = SANE_I18N ("Calibrate");
  s->opt[OPT_CALIBRATE].desc =
    SANE_I18N ("Start calibration using special sheet");
  s->opt[OPT_CALIBRATE].type = SANE_TYPE_BUTTON;
  s->opt[OPT_CALIBRATE].unit = SANE_UNIT_NONE;
  if (model->buttons & GENESYS_HAS_CALIBRATE)
    s->opt[OPT_CALIBRATE].cap =
      SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT | SANE_CAP_ADVANCED |
      SANE_CAP_AUTOMATIC;
  else
    s->opt[OPT_CALIBRATE].cap = SANE_CAP_INACTIVE;

  /* clear calibration cache button */
  s->opt[OPT_CLEAR_CALIBRATION].name = "clear-calibration";
  s->opt[OPT_CLEAR_CALIBRATION].title = SANE_I18N ("Clear calibration");
  s->opt[OPT_CLEAR_CALIBRATION].desc = SANE_I18N ("Clear calibration cache");
  s->opt[OPT_CLEAR_CALIBRATION].type = SANE_TYPE_BUTTON;
  s->opt[OPT_CLEAR_CALIBRATION].unit = SANE_UNIT_NONE;
  s->opt[OPT_CLEAR_CALIBRATION].size = 0;
  s->opt[OPT_CLEAR_CALIBRATION].constraint_type = SANE_CONSTRAINT_NONE;
  s->opt[OPT_CLEAR_CALIBRATION].cap =
    SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT | SANE_CAP_ADVANCED;

  /* force calibration cache button */
  s->opt[OPT_FORCE_CALIBRATION].name = "force-calibration";
  s->opt[OPT_FORCE_CALIBRATION].title = SANE_I18N("Force calibration");
  s->opt[OPT_FORCE_CALIBRATION].desc = SANE_I18N("Force calibration ignoring all and any calibration caches");
  s->opt[OPT_FORCE_CALIBRATION].type = SANE_TYPE_BUTTON;
  s->opt[OPT_FORCE_CALIBRATION].unit = SANE_UNIT_NONE;
  s->opt[OPT_FORCE_CALIBRATION].size = 0;
  s->opt[OPT_FORCE_CALIBRATION].constraint_type = SANE_CONSTRAINT_NONE;
  s->opt[OPT_FORCE_CALIBRATION].cap =
    SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT | SANE_CAP_ADVANCED;

    // ignore offsets option
    s->opt[OPT_IGNORE_OFFSETS].name = "ignore-internal-offsets";
    s->opt[OPT_IGNORE_OFFSETS].title = SANE_I18N("Ignore internal offsets");
    s->opt[OPT_IGNORE_OFFSETS].desc =
        SANE_I18N("Acquires the image including the internal calibration areas of the scanner");
    s->opt[OPT_IGNORE_OFFSETS].type = SANE_TYPE_BUTTON;
    s->opt[OPT_IGNORE_OFFSETS].unit = SANE_UNIT_NONE;
    s->opt[OPT_IGNORE_OFFSETS].size = 0;
    s->opt[OPT_IGNORE_OFFSETS].constraint_type = SANE_CONSTRAINT_NONE;
    s->opt[OPT_IGNORE_OFFSETS].cap = SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT |
                                     SANE_CAP_ADVANCED;

    calc_parameters(s);
}

static bool present;

// this function is passed to C API, it must not throw
static SANE_Status
check_present (SANE_String_Const devname) noexcept
{
    DBG_HELPER_ARGS(dbg, "%s detected.", devname);
    present = true;
  return SANE_STATUS_GOOD;
}

const UsbDeviceEntry& get_matching_usb_dev(std::uint16_t vendor_id, std::uint16_t product_id,
                                           std::uint16_t bcd_device)
{
    for (auto& usb_dev : *s_usb_devices) {
        if (usb_dev.matches(vendor_id, product_id, bcd_device)) {
            return usb_dev;
        }
    }

    throw SaneException("vendor 0x%x product 0x%x (bcdDevice 0x%x) "
                        "is not supported by this backend",
                        vendor_id, product_id, bcd_device);
}

static Genesys_Device* attach_usb_device(const char* devname,
                                         std::uint16_t vendor_id, std::uint16_t product_id,
                                         std::uint16_t bcd_device)
{
    const auto& usb_dev = get_matching_usb_dev(vendor_id, product_id, bcd_device);

    s_devices->emplace_back();
    Genesys_Device* dev = &s_devices->back();
    dev->file_name = devname;
    dev->vendorId = vendor_id;
    dev->productId = product_id;
    dev->model = &usb_dev.model();
    dev->usb_mode = 0; // i.e. unset
    dev->already_initialized = false;
    return dev;
}

static bool s_attach_device_by_name_evaluate_bcd_device = false;

static Genesys_Device* attach_device_by_name(SANE_String_Const devname, bool may_wait)
{
    DBG_HELPER_ARGS(dbg, " devname: %s, may_wait = %d", devname, may_wait);

    if (!devname) {
        throw SaneException("devname must not be nullptr");
    }

    for (auto& dev : *s_devices) {
        if (dev.file_name == devname) {
            DBG(DBG_info, "%s: device `%s' was already in device list\n", __func__, devname);
            return &dev;
        }
    }

  DBG(DBG_info, "%s: trying to open device `%s'\n", __func__, devname);

    UsbDevice usb_dev;

    usb_dev.open(devname);
    DBG(DBG_info, "%s: device `%s' successfully opened\n", __func__, devname);

    auto vendor_id = usb_dev.get_vendor_id();
    auto product_id = usb_dev.get_product_id();
    auto bcd_device = UsbDeviceEntry::BCD_DEVICE_NOT_SET;
    if (s_attach_device_by_name_evaluate_bcd_device) {
        // when the device is already known before scanning, we don't want to call get_bcd_device()
        // when iterating devices, as that will interfere with record/replay during testing.
        bcd_device = usb_dev.get_bcd_device();
    }
    usb_dev.close();

  /* KV-SS080 is an auxiliary device which requires a master device to be here */
    if (vendor_id == 0x04da && product_id == 0x100f) {
        present = false;
        sanei_usb_find_devices(vendor_id, 0x1006, check_present);
        sanei_usb_find_devices(vendor_id, 0x1007, check_present);
        sanei_usb_find_devices(vendor_id, 0x1010, check_present);
        if (present == false) {
            throw SaneException("master device not present");
        }
    }

    Genesys_Device* dev = attach_usb_device(devname, vendor_id, product_id, bcd_device);

    DBG(DBG_info, "%s: found %u flatbed scanner %u at %s\n", __func__, vendor_id, product_id,
        dev->file_name.c_str());

    return dev;
}

// this function is passed to C API and must not throw
static SANE_Status attach_one_device(SANE_String_Const devname) noexcept
{
    DBG_HELPER(dbg);
    return wrap_exceptions_to_status_code(__func__, [=]()
    {
        attach_device_by_name(devname, false);
    });
}

/* configuration framework functions */

// this function is passed to C API, it must not throw
static SANE_Status
config_attach_genesys(SANEI_Config __sane_unused__ *config, const char *devname,
                      void __sane_unused__ *data) noexcept
{
  /* the devname has been processed and is ready to be used
   * directly. Since the backend is an USB only one, we can
   * call sanei_usb_attach_matching_devices straight */
  sanei_usb_attach_matching_devices (devname, attach_one_device);

  return SANE_STATUS_GOOD;
}

/* probes for scanner to attach to the backend */
static void probe_genesys_devices()
{
    DBG_HELPER(dbg);
    if (is_testing_mode()) {
        attach_usb_device(get_testing_device_name().c_str(),
                          get_testing_vendor_id(), get_testing_product_id(),
                          get_testing_bcd_device());
        return;
    }

  SANEI_Config config;

    // set configuration options structure : no option for this backend
    config.descriptors = nullptr;
    config.values = nullptr;
  config.count = 0;

    auto status = sanei_configure_attach(GENESYS_CONFIG_FILE, &config,
                                         config_attach_genesys, NULL);
    if (status == SANE_STATUS_ACCESS_DENIED) {
        dbg.vlog(DBG_error0, "Critical error: Couldn't access configuration file '%s'",
                 GENESYS_CONFIG_FILE);
    }
    TIE(status);

    DBG(DBG_info, "%s: %zu devices currently attached\n", __func__, s_devices->size());
}

/**
 * This should be changed if one of the substructures of
   Genesys_Calibration_Cache change, but it must be changed if there are
   changes that don't change size -- at least for now, as we store most
   of Genesys_Calibration_Cache as is.
*/
static const char* CALIBRATION_IDENT = "sane_genesys";
static const int CALIBRATION_VERSION = 32;

bool read_calibration(std::istream& str, Genesys_Device::Calibration& calibration,
                      const std::string& path)
{
    DBG_HELPER(dbg);

    std::string ident;
    serialize(str, ident);

    if (ident != CALIBRATION_IDENT) {
        DBG(DBG_info, "%s: Incorrect calibration file '%s' header\n", __func__, path.c_str());
        return false;
    }

    size_t version;
    serialize(str, version);

    if (version != CALIBRATION_VERSION) {
        DBG(DBG_info, "%s: Incorrect calibration file '%s' version\n", __func__, path.c_str());
        return false;
    }

    calibration.clear();
    serialize(str, calibration);
    return true;
}

/**
 * reads previously cached calibration data
 * from file defined in dev->calib_file
 */
static bool sanei_genesys_read_calibration(Genesys_Device::Calibration& calibration,
                                           const std::string& path)
{
    DBG_HELPER(dbg);

    std::ifstream str;
    str.open(path);
    if (!str.is_open()) {
        DBG(DBG_info, "%s: Cannot open %s\n", __func__, path.c_str());
        return false;
    }

    return read_calibration(str, calibration, path);
}

void write_calibration(std::ostream& str, Genesys_Device::Calibration& calibration)
{
    std::string ident = CALIBRATION_IDENT;
    serialize(str, ident);
    size_t version = CALIBRATION_VERSION;
    serialize(str, version);
    serialize_newline(str);
    serialize(str, calibration);
}

static void write_calibration(Genesys_Device::Calibration& calibration, const std::string& path)
{
    DBG_HELPER(dbg);

    std::ofstream str;
    str.open(path);
    if (!str.is_open()) {
        throw SaneException("Cannot open calibration for writing");
    }
    write_calibration(str, calibration);
}

/* -------------------------- SANE API functions ------------------------- */

void sane_init_impl(SANE_Int * version_code, SANE_Auth_Callback authorize)
{
  DBG_INIT ();
    DBG_HELPER_ARGS(dbg, "authorize %s null", authorize ? "!=" : "==");
    DBG(DBG_init, "SANE Genesys backend from %s\n", PACKAGE_STRING);

    if (!is_testing_mode()) {
#ifdef HAVE_LIBUSB
        DBG(DBG_init, "SANE Genesys backend built with libusb-1.0\n");
#endif
#ifdef HAVE_LIBUSB_LEGACY
        DBG(DBG_init, "SANE Genesys backend built with libusb\n");
#endif
    }

    if (version_code) {
        *version_code = SANE_VERSION_CODE(SANE_CURRENT_MAJOR, SANE_CURRENT_MINOR, 0);
    }

    if (!is_testing_mode()) {
        sanei_usb_init();
    }

  s_scanners.init();
  s_devices.init();
  s_sane_devices.init();
    s_sane_devices_data.init();
  s_sane_devices_ptrs.init();
  genesys_init_sensor_tables();
  genesys_init_frontend_tables();
    genesys_init_gpo_tables();
    genesys_init_memory_layout_tables();
    genesys_init_motor_tables();
    genesys_init_usb_device_tables();


  DBG(DBG_info, "%s: %s endian machine\n", __func__,
#ifdef WORDS_BIGENDIAN
       "big"
#else
       "little"
#endif
    );

    // cold-plug case :detection of already connected scanners
    s_attach_device_by_name_evaluate_bcd_device = false;
    probe_genesys_devices();
}


SANE_GENESYS_API_LINKAGE
SANE_Status sane_init(SANE_Int * version_code, SANE_Auth_Callback authorize)
{
    return wrap_exceptions_to_status_code(__func__, [=]()
    {
        sane_init_impl(version_code, authorize);
    });
}

void
sane_exit_impl(void)
{
    DBG_HELPER(dbg);

    if (!is_testing_mode()) {
        sanei_usb_exit();
    }

  run_functions_at_backend_exit();
}

SANE_GENESYS_API_LINKAGE
void sane_exit()
{
    catch_all_exceptions(__func__, [](){ sane_exit_impl(); });
}

void sane_get_devices_impl(const SANE_Device *** device_list, SANE_Bool local_only)
{
    DBG_HELPER_ARGS(dbg, "local_only = %s", local_only ? "true" : "false");

    if (!is_testing_mode()) {
        // hot-plug case : detection of newly connected scanners */
        sanei_usb_scan_devices();
    }
    s_attach_device_by_name_evaluate_bcd_device = true;
    probe_genesys_devices();

    s_sane_devices->clear();
    s_sane_devices_data->clear();
    s_sane_devices_ptrs->clear();
    s_sane_devices->reserve(s_devices->size());
    s_sane_devices_data->reserve(s_devices->size());
    s_sane_devices_ptrs->reserve(s_devices->size() + 1);

    for (auto dev_it = s_devices->begin(); dev_it != s_devices->end();) {

        if (is_testing_mode()) {
            present = true;
        } else {
            present = false;
            sanei_usb_find_devices(dev_it->vendorId, dev_it->productId, check_present);
        }

        if (present) {
            s_sane_devices->emplace_back();
            s_sane_devices_data->emplace_back();
            auto& sane_device = s_sane_devices->back();
            auto& sane_device_data = s_sane_devices_data->back();
            sane_device_data.name = dev_it->file_name;
            sane_device.name = sane_device_data.name.c_str();
            sane_device.vendor = dev_it->model->vendor;
            sane_device.model = dev_it->model->model;
            sane_device.type = "flatbed scanner";
            s_sane_devices_ptrs->push_back(&sane_device);
            dev_it++;
        } else {
            dev_it = s_devices->erase(dev_it);
        }
    }
    s_sane_devices_ptrs->push_back(nullptr);

    *const_cast<SANE_Device***>(device_list) = s_sane_devices_ptrs->data();
}

SANE_GENESYS_API_LINKAGE
SANE_Status sane_get_devices(const SANE_Device *** device_list, SANE_Bool local_only)
{
    return wrap_exceptions_to_status_code(__func__, [=]()
    {
        sane_get_devices_impl(device_list, local_only);
    });
}

static void sane_open_impl(SANE_String_Const devicename, SANE_Handle * handle)
{
    DBG_HELPER_ARGS(dbg, "devicename = %s", devicename);
    Genesys_Device* dev = nullptr;

  /* devicename="" or devicename="genesys" are default values that use
   * first available device
   */
    if (devicename[0] && strcmp ("genesys", devicename) != 0) {
      /* search for the given devicename in the device list */
        for (auto& d : *s_devices) {
            if (d.file_name == devicename) {
                dev = &d;
                break;
            }
        }

        if (dev) {
            DBG(DBG_info, "%s: found `%s' in devlist\n", __func__, dev->file_name.c_str());
        } else if (is_testing_mode()) {
            DBG(DBG_info, "%s: couldn't find `%s' in devlist, not attaching", __func__, devicename);
        } else {
            DBG(DBG_info, "%s: couldn't find `%s' in devlist, trying attach\n", __func__,
                devicename);
            dbg.status("attach_device_by_name");
            dev = attach_device_by_name(devicename, true);
            dbg.clear();
        }
    } else {
        // empty devicename or "genesys" -> use first device
        if (!s_devices->empty()) {
            dev = &s_devices->front();
            DBG(DBG_info, "%s: empty devicename, trying `%s'\n", __func__, dev->file_name.c_str());
        }
    }

    if (!dev) {
        throw SaneException("could not find the device to open: %s", devicename);
    }

    if (is_testing_mode()) {
        // during testing we need to initialize dev->model before test scanner interface is created
        // as that it needs to know what type of chip it needs to mimic.
        auto vendor_id = get_testing_vendor_id();
        auto product_id = get_testing_product_id();
        auto bcd_device = get_testing_bcd_device();

        dev->model = &get_matching_usb_dev(vendor_id, product_id, bcd_device).model();

        auto interface = std::unique_ptr<TestScannerInterface>{
                new TestScannerInterface{dev, vendor_id, product_id, bcd_device}};
        interface->set_checkpoint_callback(get_testing_checkpoint_callback());
        dev->interface = std::move(interface);

        dev->interface->get_usb_device().open(dev->file_name.c_str());
    } else {
        dev->interface = std::unique_ptr<ScannerInterfaceUsb>{new ScannerInterfaceUsb{dev}};

        dbg.vstatus("open device '%s'", dev->file_name.c_str());
        dev->interface->get_usb_device().open(dev->file_name.c_str());
        dbg.clear();

        auto bcd_device = dev->interface->get_usb_device().get_bcd_device();

        dev->model = &get_matching_usb_dev(dev->vendorId, dev->productId, bcd_device).model();
    }

    dbg.vlog(DBG_info, "Opened device %s", dev->model->name);

    if (has_flag(dev->model->flags, ModelFlag::UNTESTED)) {
        DBG(DBG_error0, "WARNING: Your scanner is not fully supported or at least \n");
        DBG(DBG_error0, "         had only limited testing. Please be careful and \n");
        DBG(DBG_error0, "         report any failure/success to \n");
        DBG(DBG_error0, "         sane-devel@alioth-lists.debian.net. Please provide as many\n");
        DBG(DBG_error0, "         details as possible, e.g. the exact name of your\n");
        DBG(DBG_error0, "         scanner and what does (not) work.\n");
    }

  s_scanners->push_back(Genesys_Scanner());
  auto* s = &s_scanners->back();

    s->dev = dev;
    s->scanning = false;
    dev->parking = false;
    dev->read_active = false;
    dev->force_calibration = 0;
    dev->line_count = 0;

  *handle = s;

    if (!dev->already_initialized) {
        sanei_genesys_init_structs (dev);
    }

    dev->cmd_set = create_cmd_set(dev->model->asic_type);

    init_options(s);

    DBG_INIT();

    // FIXME: we create sensor tables for the sensor, this should happen when we know which sensor
    // we will select
    dev->cmd_set->init(dev);

    // some hardware capabilities are detected through sensors
    dev->cmd_set->update_hardware_sensors (s);
}

SANE_GENESYS_API_LINKAGE
SANE_Status sane_open(SANE_String_Const devicename, SANE_Handle* handle)
{
    return wrap_exceptions_to_status_code(__func__, [=]()
    {
        sane_open_impl(devicename, handle);
    });
}

void
sane_close_impl(SANE_Handle handle)
{
    DBG_HELPER(dbg);

  /* remove handle from list of open handles: */
  auto it = s_scanners->end();
  for (auto it2 = s_scanners->begin(); it2 != s_scanners->end(); it2++)
    {
      if (&*it2 == handle) {
          it = it2;
          break;
        }
    }
  if (it == s_scanners->end())
    {
      DBG(DBG_error, "%s: invalid handle %p\n", __func__, handle);
      return;			/* oops, not a handle we know about */
    }

    auto* dev = it->dev;

    // eject document for sheetfed scanners
    if (dev->model->is_sheetfed) {
        catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); });
    } else {
        // in case scanner is parking, wait for the head to reach home position
        if (dev->parking) {
            sanei_genesys_wait_for_home(dev);
        }
    }

    // enable power saving before leaving
    dev->cmd_set->save_power(dev, true);

    // here is the place to store calibration cache
    if (dev->force_calibration == 0 && !is_testing_mode()) {
        catch_all_exceptions(__func__, [&](){ write_calibration(dev->calibration_cache,
                                                                dev->calib_file); });
    }

    dev->already_initialized = false;
    dev->clear();

    // LAMP OFF : same register across all the ASICs */
    dev->interface->write_register(0x03, 0x00);

    catch_all_exceptions(__func__, [&](){ dev->interface->get_usb_device().clear_halt(); });

    // we need this to avoid these ASIC getting stuck in bulk writes
    catch_all_exceptions(__func__, [&](){ dev->interface->get_usb_device().reset(); });

    // not freeing dev because it's in the dev list
    catch_all_exceptions(__func__, [&](){ dev->interface->get_usb_device().close(); });

    s_scanners->erase(it);
}

SANE_GENESYS_API_LINKAGE
void sane_close(SANE_Handle handle)
{
    catch_all_exceptions(__func__, [=]()
    {
        sane_close_impl(handle);
    });
}

const SANE_Option_Descriptor *
sane_get_option_descriptor_impl(SANE_Handle handle, SANE_Int option)
{
    DBG_HELPER(dbg);
    Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle);

    if (static_cast<unsigned>(option) >= NUM_OPTIONS) {
        return nullptr;
    }

  DBG(DBG_io2, "%s: option = %s (%d)\n", __func__, s->opt[option].name, option);
  return s->opt + option;
}


SANE_GENESYS_API_LINKAGE
const SANE_Option_Descriptor* sane_get_option_descriptor(SANE_Handle handle, SANE_Int option)
{
    const SANE_Option_Descriptor* ret = nullptr;
    catch_all_exceptions(__func__, [&]()
    {
        ret = sane_get_option_descriptor_impl(handle, option);
    });
    return ret;
}

static void print_option(DebugMessageHelper& dbg, const Genesys_Scanner& s, int option, void* val)
{
    switch (s.opt[option].type) {
        case SANE_TYPE_INT: {
            dbg.vlog(DBG_proc, "value: %d", *reinterpret_cast<SANE_Word*>(val));
            return;
        }
        case SANE_TYPE_BOOL: {
            dbg.vlog(DBG_proc, "value: %s", *reinterpret_cast<SANE_Bool*>(val) ? "true" : "false");
            return;
        }
        case SANE_TYPE_FIXED: {
            dbg.vlog(DBG_proc, "value: %f", fixed_to_float(*reinterpret_cast<SANE_Word*>(val)));
            return;
        }
        case SANE_TYPE_STRING: {
            dbg.vlog(DBG_proc, "value: %s", reinterpret_cast<char*>(val));
            return;
        }
        default: break;
    }
    dbg.log(DBG_proc, "value: (non-printable)");
}

static void get_option_value(Genesys_Scanner* s, int option, void* val)
{
    DBG_HELPER_ARGS(dbg, "option: %s (%d)", s->opt[option].name, option);
    auto* dev = s->dev;
  unsigned int i;
    SANE_Word* table = nullptr;
  std::vector<uint16_t> gamma_table;
  unsigned option_size = 0;

    const Genesys_Sensor* sensor = nullptr;
    if (sanei_genesys_has_sensor(dev, dev->settings.xres, dev->settings.get_channels(),
                                 dev->settings.scan_method))
    {
        sensor = &sanei_genesys_find_sensor(dev, dev->settings.xres,
                                            dev->settings.get_channels(),
                                            dev->settings.scan_method);
    }

  switch (option)
    {
      /* geometry */
    case OPT_TL_X:
        *reinterpret_cast<SANE_Word*>(val) = s->pos_top_left_x;
        break;
    case OPT_TL_Y:
        *reinterpret_cast<SANE_Word*>(val) = s->pos_top_left_y;
        break;
    case OPT_BR_X:
        *reinterpret_cast<SANE_Word*>(val) = s->pos_bottom_right_x;
        break;
    case OPT_BR_Y:
        *reinterpret_cast<SANE_Word*>(val) = s->pos_bottom_right_y;
        break;
      /* word options: */
    case OPT_NUM_OPTS:
        *reinterpret_cast<SANE_Word*>(val) = NUM_OPTIONS;
        break;
    case OPT_RESOLUTION:
        *reinterpret_cast<SANE_Word*>(val) = s->resolution;
        break;
    case OPT_BIT_DEPTH:
        *reinterpret_cast<SANE_Word*>(val) = s->bit_depth;
        break;
    case OPT_PREVIEW:
        *reinterpret_cast<SANE_Word*>(val) = s->preview;
        break;
    case OPT_LAMP_OFF:
        *reinterpret_cast<SANE_Word*>(val) = s->lamp_off;
        break;
    case OPT_LAMP_OFF_TIME:
        *reinterpret_cast<SANE_Word*>(val) = s->lamp_off_time;
        break;
    case OPT_CONTRAST:
        *reinterpret_cast<SANE_Word*>(val) = s->contrast;
        break;
    case OPT_BRIGHTNESS:
        *reinterpret_cast<SANE_Word*>(val) = s->brightness;
        break;
    case OPT_EXPIRATION_TIME:
        *reinterpret_cast<SANE_Word*>(val) = s->expiration_time;
        break;
    case OPT_CUSTOM_GAMMA:
        *reinterpret_cast<SANE_Word*>(val) = s->custom_gamma;
        break;

      /* string options: */
    case OPT_MODE:
        std::strcpy(reinterpret_cast<char*>(val), s->mode.c_str());
        break;
    case OPT_COLOR_FILTER:
        std::strcpy(reinterpret_cast<char*>(val), s->color_filter.c_str());
        break;
    case OPT_CALIBRATION_FILE:
        std::strcpy(reinterpret_cast<char*>(val), s->calibration_file.c_str());
        break;
    case OPT_SOURCE:
        std::strcpy(reinterpret_cast<char*>(val), scan_method_to_option_string(s->scan_method));
        break;

      /* word array options */
    case OPT_GAMMA_VECTOR:
        if (!sensor)
            throw SaneException("Unsupported scanner mode selected");

        table = reinterpret_cast<SANE_Word*>(val);
            if (s->color_filter == "Red") {
                gamma_table = get_gamma_table(dev, *sensor, GENESYS_RED);
            } else if (s->color_filter == "Blue") {
                gamma_table = get_gamma_table(dev, *sensor, GENESYS_BLUE);
            } else {
                gamma_table = get_gamma_table(dev, *sensor, GENESYS_GREEN);
            }
        option_size = s->opt[option].size / sizeof (SANE_Word);
        if (gamma_table.size() != option_size) {
            throw std::runtime_error("The size of the gamma tables does not match");
        }
        for (i = 0; i < option_size; i++) {
            table[i] = gamma_table[i];
        }
        break;
    case OPT_GAMMA_VECTOR_R:
        if (!sensor)
            throw SaneException("Unsupported scanner mode selected");

        table = reinterpret_cast<SANE_Word*>(val);
            gamma_table = get_gamma_table(dev, *sensor, GENESYS_RED);
        option_size = s->opt[option].size / sizeof (SANE_Word);
        if (gamma_table.size() != option_size) {
            throw std::runtime_error("The size of the gamma tables does not match");
        }
        for (i = 0; i < option_size; i++) {
            table[i] = gamma_table[i];
	}
      break;
    case OPT_GAMMA_VECTOR_G:
        if (!sensor)
            throw SaneException("Unsupported scanner mode selected");

        table = reinterpret_cast<SANE_Word*>(val);
            gamma_table = get_gamma_table(dev, *sensor, GENESYS_GREEN);
        option_size = s->opt[option].size / sizeof (SANE_Word);
        if (gamma_table.size() != option_size) {
            throw std::runtime_error("The size of the gamma tables does not match");
        }
        for (i = 0; i < option_size; i++) {
            table[i] = gamma_table[i];
        }
      break;
    case OPT_GAMMA_VECTOR_B:
        if (!sensor)
            throw SaneException("Unsupported scanner mode selected");

        table = reinterpret_cast<SANE_Word*>(val);
            gamma_table = get_gamma_table(dev, *sensor, GENESYS_BLUE);
        option_size = s->opt[option].size / sizeof (SANE_Word);
        if (gamma_table.size() != option_size) {
            throw std::runtime_error("The size of the gamma tables does not match");
        }
        for (i = 0; i < option_size; i++) {
            table[i] = gamma_table[i];
        }
      break;
      /* sensors */
    case OPT_SCAN_SW:
    case OPT_FILE_SW:
    case OPT_EMAIL_SW:
    case OPT_COPY_SW:
    case OPT_PAGE_LOADED_SW:
    case OPT_OCR_SW:
    case OPT_POWER_SW:
    case OPT_EXTRA_SW:
    case OPT_TRANSP_SW:
        s->dev->cmd_set->update_hardware_sensors(s);
        *reinterpret_cast<SANE_Bool*>(val) = s->buttons[genesys_option_to_button(option)].read();
        break;

        case OPT_NEED_CALIBRATION_SW: {
            if (!sensor) {
                throw SaneException("Unsupported scanner mode selected");
            }

            // scanner needs calibration for current mode unless a matching calibration cache is
            // found

            bool result = true;

            auto session = dev->cmd_set->calculate_scan_session(dev, *sensor, dev->settings);

            for (auto& cache : dev->calibration_cache) {
                if (sanei_genesys_is_compatible_calibration(dev, session, &cache, false)) {
                    *reinterpret_cast<SANE_Bool*>(val) = SANE_FALSE;
                }
            }
            *reinterpret_cast<SANE_Bool*>(val) = result;
            break;
        }
    default:
      DBG(DBG_warn, "%s: can't get unknown option %d\n", __func__, option);
    }
    print_option(dbg, *s, option, val);
}

/** @brief set calibration file value
 * Set calibration file value. Load new cache values from file if it exists,
 * else creates the file*/
static void set_calibration_value(Genesys_Scanner* s, const char* val)
{
    DBG_HELPER(dbg);
    auto dev = s->dev;

    std::string new_calib_path = val;
    Genesys_Device::Calibration new_calibration;

    bool is_calib_success = false;
    catch_all_exceptions(__func__, [&]()
    {
        is_calib_success = sanei_genesys_read_calibration(new_calibration, new_calib_path);
    });

    if (!is_calib_success) {
        return;
    }

    dev->calibration_cache = std::move(new_calibration);
    dev->calib_file = new_calib_path;
    s->calibration_file = new_calib_path;
    DBG(DBG_info, "%s: Calibration filename set to '%s':\n", __func__, new_calib_path.c_str());
}

/* sets an option , called by sane_control_option */
static void set_option_value(Genesys_Scanner* s, int option, void *val, SANE_Int* myinfo)
{
    DBG_HELPER_ARGS(dbg, "option: %s (%d)", s->opt[option].name, option);
    print_option(dbg, *s, option, val);

    auto* dev = s->dev;

  SANE_Word *table;
  unsigned int i;
  unsigned option_size = 0;

    switch (option) {
    case OPT_TL_X:
        s->pos_top_left_x = *reinterpret_cast<SANE_Word*>(val);
        calc_parameters(s);
        *myinfo |= SANE_INFO_RELOAD_PARAMS;
        break;
    case OPT_TL_Y:
        s->pos_top_left_y = *reinterpret_cast<SANE_Word*>(val);
        calc_parameters(s);
        *myinfo |= SANE_INFO_RELOAD_PARAMS;
        break;
    case OPT_BR_X:
        s->pos_bottom_right_x = *reinterpret_cast<SANE_Word*>(val);
        calc_parameters(s);
        *myinfo |= SANE_INFO_RELOAD_PARAMS;
        break;
    case OPT_BR_Y:
        s->pos_bottom_right_y = *reinterpret_cast<SANE_Word*>(val);
        calc_parameters(s);
        *myinfo |= SANE_INFO_RELOAD_PARAMS;
        break;
    case OPT_RESOLUTION:
        s->resolution = *reinterpret_cast<SANE_Word*>(val);
        calc_parameters(s);
        *myinfo |= SANE_INFO_RELOAD_PARAMS;
        break;
    case OPT_LAMP_OFF:
        s->lamp_off = *reinterpret_cast<SANE_Word*>(val);
        calc_parameters(s);
        *myinfo |= SANE_INFO_RELOAD_PARAMS;
        break;
    case OPT_PREVIEW:
        s->preview = *reinterpret_cast<SANE_Word*>(val);
        calc_parameters(s);
        *myinfo |= SANE_INFO_RELOAD_PARAMS;
        break;
    case OPT_BRIGHTNESS:
        s->brightness = *reinterpret_cast<SANE_Word*>(val);
        calc_parameters(s);
        *myinfo |= SANE_INFO_RELOAD_PARAMS;
        break;
    case OPT_CONTRAST:
        s->contrast = *reinterpret_cast<SANE_Word*>(val);
        calc_parameters(s);
        *myinfo |= SANE_INFO_RELOAD_PARAMS;
        break;
    /* software enhancement functions only apply to 8 or 1 bits data */
    case OPT_BIT_DEPTH:
        s->bit_depth = *reinterpret_cast<SANE_Word*>(val);
        if(s->bit_depth>8)
        {
          DISABLE(OPT_CONTRAST);
          DISABLE(OPT_BRIGHTNESS);
            } else {
          ENABLE(OPT_CONTRAST);
          ENABLE(OPT_BRIGHTNESS);
        }
        calc_parameters(s);
      *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS;
      break;
    case OPT_SOURCE: {
        auto scan_method = option_string_to_scan_method(reinterpret_cast<const char*>(val));
        if (s->scan_method != scan_method) {
            s->scan_method = scan_method;

            set_xy_range_option_values(*s);
            set_resolution_option_values(*s, false);

            *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS;
        }
        break;
    }
        case OPT_MODE: {
            s->mode = reinterpret_cast<const char*>(val);

            if (s->mode == SANE_VALUE_SCAN_MODE_GRAY) {
                if (dev->model->asic_type != AsicType::GL646 || !dev->model->is_cis) {
                    ENABLE(OPT_COLOR_FILTER);
                }
                create_bpp_list(s, dev->model->bpp_gray_values);
                s->bit_depth = dev->model->bpp_gray_values[0];
            } else {
                DISABLE(OPT_COLOR_FILTER);
                create_bpp_list(s, dev->model->bpp_color_values);
                s->bit_depth = dev->model->bpp_color_values[0];
            }

            calc_parameters(s);

      /* if custom gamma, toggle gamma table options according to the mode */
      if (s->custom_gamma)
	{
          if (s->mode == SANE_VALUE_SCAN_MODE_COLOR)
	    {
	      DISABLE (OPT_GAMMA_VECTOR);
	      ENABLE (OPT_GAMMA_VECTOR_R);
	      ENABLE (OPT_GAMMA_VECTOR_G);
	      ENABLE (OPT_GAMMA_VECTOR_B);
	    }
	  else
	    {
	      ENABLE (OPT_GAMMA_VECTOR);
	      DISABLE (OPT_GAMMA_VECTOR_R);
	      DISABLE (OPT_GAMMA_VECTOR_G);
	      DISABLE (OPT_GAMMA_VECTOR_B);
	    }
	}

      *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS;
      break;
        }
    case OPT_COLOR_FILTER:
      s->color_filter = reinterpret_cast<const char*>(val);
        calc_parameters(s);
      break;
        case OPT_CALIBRATION_FILE: {
            if (dev->force_calibration == 0) {
                set_calibration_value(s, reinterpret_cast<const char*>(val));
            }
            break;
        }
        case OPT_LAMP_OFF_TIME: {
            if (*reinterpret_cast<SANE_Word*>(val) != s->lamp_off_time) {
                s->lamp_off_time = *reinterpret_cast<SANE_Word*>(val);
                    dev->cmd_set->set_powersaving(dev, s->lamp_off_time);
            }
            break;
        }
        case OPT_EXPIRATION_TIME: {
            if (*reinterpret_cast<SANE_Word*>(val) != s->expiration_time) {
                s->expiration_time = *reinterpret_cast<SANE_Word*>(val);
            }
            break;
        }
        case OPT_CUSTOM_GAMMA: {
      *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS;
        s->custom_gamma = *reinterpret_cast<SANE_Bool*>(val);

        if (s->custom_gamma) {
          if (s->mode == SANE_VALUE_SCAN_MODE_COLOR)
	    {
	      DISABLE (OPT_GAMMA_VECTOR);
	      ENABLE (OPT_GAMMA_VECTOR_R);
	      ENABLE (OPT_GAMMA_VECTOR_G);
	      ENABLE (OPT_GAMMA_VECTOR_B);
	    }
	  else
	    {
	      ENABLE (OPT_GAMMA_VECTOR);
	      DISABLE (OPT_GAMMA_VECTOR_R);
	      DISABLE (OPT_GAMMA_VECTOR_G);
	      DISABLE (OPT_GAMMA_VECTOR_B);
	    }
	}
      else
	{
	  DISABLE (OPT_GAMMA_VECTOR);
	  DISABLE (OPT_GAMMA_VECTOR_R);
	  DISABLE (OPT_GAMMA_VECTOR_G);
	  DISABLE (OPT_GAMMA_VECTOR_B);
                for (auto& table : dev->gamma_override_tables) {
                    table.clear();
                }
            }
            break;
        }

        case OPT_GAMMA_VECTOR: {
            table = reinterpret_cast<SANE_Word*>(val);
            option_size = s->opt[option].size / sizeof (SANE_Word);

            dev->gamma_override_tables[GENESYS_RED].resize(option_size);
            dev->gamma_override_tables[GENESYS_GREEN].resize(option_size);
            dev->gamma_override_tables[GENESYS_BLUE].resize(option_size);
            for (i = 0; i < option_size; i++) {
                dev->gamma_override_tables[GENESYS_RED][i] = table[i];
                dev->gamma_override_tables[GENESYS_GREEN][i] = table[i];
                dev->gamma_override_tables[GENESYS_BLUE][i] = table[i];
            }
            break;
        }
        case OPT_GAMMA_VECTOR_R: {
            table = reinterpret_cast<SANE_Word*>(val);
            option_size = s->opt[option].size / sizeof (SANE_Word);
            dev->gamma_override_tables[GENESYS_RED].resize(option_size);
            for (i = 0; i < option_size; i++) {
                dev->gamma_override_tables[GENESYS_RED][i] = table[i];
            }
            break;
        }
        case OPT_GAMMA_VECTOR_G: {
            table = reinterpret_cast<SANE_Word*>(val);
            option_size = s->opt[option].size / sizeof (SANE_Word);
            dev->gamma_override_tables[GENESYS_GREEN].resize(option_size);
            for (i = 0; i < option_size; i++) {
                dev->gamma_override_tables[GENESYS_GREEN][i] = table[i];
            }
            break;
        }
        case OPT_GAMMA_VECTOR_B: {
            table = reinterpret_cast<SANE_Word*>(val);
            option_size = s->opt[option].size / sizeof (SANE_Word);
            dev->gamma_override_tables[GENESYS_BLUE].resize(option_size);
            for (i = 0; i < option_size; i++) {
                dev->gamma_override_tables[GENESYS_BLUE][i] = table[i];
            }
            break;
        }
        case OPT_CALIBRATE: {
            auto& sensor = sanei_genesys_find_sensor_for_write(dev, dev->settings.xres,
                                                               dev->settings.get_channels(),
                                                               dev->settings.scan_method);
            catch_all_exceptions(__func__, [&]()
            {
            dev->cmd_set->save_power(dev, false);
            genesys_scanner_calibration(dev, sensor);
            });
            catch_all_exceptions(__func__, [&]()
            {
            dev->cmd_set->save_power(dev, true);
            });
            *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS;
            break;
        }
        case OPT_CLEAR_CALIBRATION: {
            dev->calibration_cache.clear();

            // remove file
            unlink(dev->calib_file.c_str());
            // signals that sensors will have to be read again
            *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS;
            break;
        }
        case OPT_FORCE_CALIBRATION: {
            dev->force_calibration = 1;
            dev->calibration_cache.clear();
            dev->calib_file.clear();

            // signals that sensors will have to be read again
            *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS;
            break;
        }

        case OPT_IGNORE_OFFSETS: {
            dev->ignore_offsets = true;
            break;
        }
        default: {
            DBG(DBG_warn, "%s: can't set unknown option %d\n", __func__, option);
        }
    }
}


/* sets and gets scanner option values */
void sane_control_option_impl(SANE_Handle handle, SANE_Int option,
                              SANE_Action action, void *val, SANE_Int * info)
{
    Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle);
    auto action_str = (action == SANE_ACTION_GET_VALUE) ? "get" :
                      (action == SANE_ACTION_SET_VALUE) ? "set" :
                      (action == SANE_ACTION_SET_AUTO) ? "set_auto" : "unknown";
    DBG_HELPER_ARGS(dbg, "action = %s, option = %s (%d)", action_str,
                    s->opt[option].name, option);

  SANE_Word cap;
  SANE_Int myinfo = 0;

    if (info) {
        *info = 0;
    }

    if (s->scanning) {
        throw SaneException(SANE_STATUS_DEVICE_BUSY,
                            "don't call this function while scanning (option = %s (%d))",
                            s->opt[option].name, option);
    }
    if (option >= NUM_OPTIONS || option < 0) {
        throw SaneException("option %d >= NUM_OPTIONS || option < 0", option);
    }

  cap = s->opt[option].cap;

    if (!SANE_OPTION_IS_ACTIVE (cap)) {
        throw SaneException("option %d is inactive", option);
    }

    switch (action) {
        case SANE_ACTION_GET_VALUE:
            get_option_value(s, option, val);
            break;

        case SANE_ACTION_SET_VALUE:
            if (!SANE_OPTION_IS_SETTABLE (cap)) {
                throw SaneException("option %d is not settable", option);
            }

            TIE(sanei_constrain_value(s->opt + option, val, &myinfo));

            set_option_value(s, option, val, &myinfo);
            break;

        case SANE_ACTION_SET_AUTO:
            throw SaneException("SANE_ACTION_SET_AUTO unsupported since no option "
                                "has SANE_CAP_AUTOMATIC");
        default:
            throw SaneException("unknown action %d for option %d", action, option);
    }

  if (info)
    *info = myinfo;
}

SANE_GENESYS_API_LINKAGE
SANE_Status sane_control_option(SANE_Handle handle, SANE_Int option,
                                           SANE_Action action, void *val, SANE_Int * info)
{
    return wrap_exceptions_to_status_code(__func__, [=]()
    {
        sane_control_option_impl(handle, option, action, val, info);
    });
}

void sane_get_parameters_impl(SANE_Handle handle, SANE_Parameters* params)
{
    DBG_HELPER(dbg);
    Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle);
    auto* dev = s->dev;

  /* don't recompute parameters once data reading is active, ie during scan */
    if (!dev->read_active) {
        calc_parameters(s);
    }
    if (params) {
      *params = s->params;

      /* in the case of a sheetfed scanner, when full height is specified
       * we override the computed line number with -1 to signal that we
       * don't know the real document height.
       * We don't do that doing buffering image for digital processing
       */
        if (dev->model->is_sheetfed &&
            s->pos_bottom_right_y == s->opt[OPT_BR_Y].constraint.range->max)
        {
            params->lines = -1;
        }
    }
    debug_dump(DBG_proc, *params);
}

SANE_GENESYS_API_LINKAGE
SANE_Status sane_get_parameters(SANE_Handle handle, SANE_Parameters* params)
{
    return wrap_exceptions_to_status_code(__func__, [=]()
    {
        sane_get_parameters_impl(handle, params);
    });
}

void sane_start_impl(SANE_Handle handle)
{
    DBG_HELPER(dbg);
    Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle);
    auto* dev = s->dev;

    if (s->pos_top_left_x >= s->pos_bottom_right_x) {
        throw SaneException("top left x >= bottom right x");
    }
    if (s->pos_top_left_y >= s->pos_bottom_right_y) {
        throw SaneException("top left y >= bottom right y");
    }

    // fetch stored calibration
    if (dev->force_calibration == 0) {
        auto path = calibration_filename(dev);
        s->calibration_file = path;
        dev->calib_file = path;
        DBG(DBG_info, "%s: Calibration filename set to:\n", __func__);
        DBG(DBG_info, "%s: >%s<\n", __func__, dev->calib_file.c_str());

        catch_all_exceptions(__func__, [&]()
        {
            sanei_genesys_read_calibration(dev->calibration_cache, dev->calib_file);
        });
    }

    // First make sure we have a current parameter set.  Some of the
    // parameters will be overwritten below, but that's OK.

    calc_parameters(s);
    genesys_start_scan(dev, s->lamp_off);

    s->scanning = true;
}

SANE_GENESYS_API_LINKAGE
SANE_Status sane_start(SANE_Handle handle)
{
    return wrap_exceptions_to_status_code(__func__, [=]()
    {
        sane_start_impl(handle);
    });
}

// returns SANE_STATUS_GOOD if there are more data, SANE_STATUS_EOF otherwise
SANE_Status sane_read_impl(SANE_Handle handle, SANE_Byte * buf, SANE_Int max_len, SANE_Int* len)
{
    DBG_HELPER(dbg);
    Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle);
  size_t local_len;

    if (!s) {
        throw SaneException("handle is nullptr");
    }

    auto* dev = s->dev;
    if (!dev) {
        throw SaneException("dev is nullptr");
    }

    if (!buf) {
        throw SaneException("buf is nullptr");
    }

    if (!len) {
        throw SaneException("len is nullptr");
    }

  *len = 0;

    if (!s->scanning) {
        throw SaneException(SANE_STATUS_CANCELLED,
                            "scan was cancelled, is over or has not been initiated yet");
    }

  DBG(DBG_proc, "%s: start, %d maximum bytes required\n", __func__, max_len);
    DBG(DBG_io2, "%s: bytes_to_read=%zu, total_bytes_read=%zu\n", __func__,
        dev->total_bytes_to_read, dev->total_bytes_read);

  if(dev->total_bytes_read>=dev->total_bytes_to_read)
    {
      DBG(DBG_proc, "%s: nothing more to scan: EOF\n", __func__);

      /* issue park command immediately in case scanner can handle it
       * so we save time */
        if (!dev->model->is_sheetfed && !has_flag(dev->model->flags, ModelFlag::MUST_WAIT) &&
            !dev->parking)
        {
            dev->cmd_set->move_back_home(dev, false);
            dev->parking = true;
        }
        return SANE_STATUS_EOF;
    }

  local_len = max_len;

    genesys_read_ordered_data(dev, buf, &local_len);

  *len = local_len;
    if (local_len > static_cast<std::size_t>(max_len)) {
        dbg.log(DBG_error, "error: returning incorrect length");
    }
  DBG(DBG_proc, "%s: %d bytes returned\n", __func__, *len);
    return SANE_STATUS_GOOD;
}

SANE_GENESYS_API_LINKAGE
SANE_Status sane_read(SANE_Handle handle, SANE_Byte * buf, SANE_Int max_len, SANE_Int* len)
{
    return wrap_exceptions_to_status_code_return(__func__, [=]()
    {
        return sane_read_impl(handle, buf, max_len, len);
    });
}

void sane_cancel_impl(SANE_Handle handle)
{
    DBG_HELPER(dbg);
    Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle);
    auto* dev = s->dev;

    s->scanning = false;
    dev->read_active = false;

    // no need to end scan if we are parking the head
    if (!dev->parking) {
        dev->cmd_set->end_scan(dev, &dev->reg, true);
    }

    // park head if flatbed scanner
    if (!dev->model->is_sheetfed) {
        if (!dev->parking) {
            dev->cmd_set->move_back_home(dev, has_flag(dev->model->flags, ModelFlag::MUST_WAIT));
            dev->parking = !has_flag(dev->model->flags, ModelFlag::MUST_WAIT);
        }
    } else {
        // in case of sheetfed scanners, we have to eject the document if still present
        dev->cmd_set->eject_document(dev);
    }

    // enable power saving mode unless we are parking ....
    if (!dev->parking) {
        dev->cmd_set->save_power(dev, true);
    }
}

SANE_GENESYS_API_LINKAGE
void sane_cancel(SANE_Handle handle)
{
    catch_all_exceptions(__func__, [=]() { sane_cancel_impl(handle); });
}

void sane_set_io_mode_impl(SANE_Handle handle, SANE_Bool non_blocking)
{
    DBG_HELPER_ARGS(dbg, "handle = %p, non_blocking = %s", handle,
                    non_blocking == SANE_TRUE ? "true" : "false");
    Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle);

    if (!s->scanning) {
        throw SaneException("not scanning");
    }
    if (non_blocking) {
        throw SaneException(SANE_STATUS_UNSUPPORTED);
    }
}

SANE_GENESYS_API_LINKAGE
SANE_Status sane_set_io_mode(SANE_Handle handle, SANE_Bool non_blocking)
{
    return wrap_exceptions_to_status_code(__func__, [=]()
    {
        sane_set_io_mode_impl(handle, non_blocking);
    });
}

void sane_get_select_fd_impl(SANE_Handle handle, SANE_Int* fd)
{
    DBG_HELPER_ARGS(dbg, "handle = %p, fd = %p", handle, reinterpret_cast<void*>(fd));
    Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle);

    if (!s->scanning) {
        throw SaneException("not scanning");
    }
    throw SaneException(SANE_STATUS_UNSUPPORTED);
}

SANE_GENESYS_API_LINKAGE
SANE_Status sane_get_select_fd(SANE_Handle handle, SANE_Int* fd)
{
    return wrap_exceptions_to_status_code(__func__, [=]()
    {
        sane_get_select_fd_impl(handle, fd);
    });
}

GenesysButtonName genesys_option_to_button(int option)
{
    switch (option) {
    case OPT_SCAN_SW: return BUTTON_SCAN_SW;
    case OPT_FILE_SW: return BUTTON_FILE_SW;
    case OPT_EMAIL_SW: return BUTTON_EMAIL_SW;
    case OPT_COPY_SW: return BUTTON_COPY_SW;
    case OPT_PAGE_LOADED_SW: return BUTTON_PAGE_LOADED_SW;
    case OPT_OCR_SW: return BUTTON_OCR_SW;
    case OPT_POWER_SW: return BUTTON_POWER_SW;
    case OPT_EXTRA_SW: return BUTTON_EXTRA_SW;
    case OPT_TRANSP_SW: return BUTTON_TRANSP_SW;
    default: throw std::runtime_error("Unknown option to convert to button index");
    }
}

} // namespace genesys