/** @file dlo_mode.c
 *
 *  @brief Implementation of the screen mode-related functions.
 *
 *  See dlo_mode.h for more information.
 *
 *  DisplayLink Open Source Software (libdlo)
 *  Copyright (C) 2009, DisplayLink
 *  www.displaylink.com
 *
 *  This library is free software; you can redistribute it and/or modify it under
 *  the terms of the GNU Library General Public License as published by the Free
 *  Software Foundation; LGPL version 2, dated June 1991.
 *
 *  This library 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 Library General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU Library General Public License
 *  along with this library; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <stdio.h>
#include <string.h>
#include "dlo_defs.h"
#include "dlo_mode.h"
#include "dlo_data.h"
#include "dlo_usb.h"


/* File-scope defines ------------------------------------------------------------------*/


/** Pre-defined EDID header used to check that data read from a device is valid.
 */
const uint8_t header[8] =
{
  0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00
};

/** Constant used for deciding if a host is big- or little endian.
 */
const uint32_t endian = 1;

/** Unlock video registers command.
 */
#define WRITE_VIDREG_LOCK "\xAF\x20\xFF\x00"

/** Lock video registers command.
 */
#define WRITE_VIDREG_UNLOCK "\xAF\x20\xFF\xFF\xAF\xA0"

/** Return non-zero if the host is big endian or zero if the host is little endian.
 */
#define IS_BIGENDIAN() ((*(char*)&endian) == '\0')

/** Convert a 32 bit little endian value into a host byte-order value.
 *
 *  @param  val  Little-endian 32 bit value to convert.
 *
 *  @return  Value in host byte order.
 */
#define LETOHL(val) (IS_BIGENDIAN() ? swap_endian_l(val) : val)

/** Convert a 16 bit little endian value into a host byte-order value.
 *
 *  @param  val  Little-endian 16 bit value to convert.
 *
 *  @return  Value in host byte order.
 */
#define LETOHS(val) (IS_BIGENDIAN() ? swap_endian_s(val) : val)

/** Initialise a @a dlo_mode_data array entry.
 *
 *  @param  idx       Array index.
 *  @param  mwidth    Mode width (pixels).
 *  @param  mheight   Mode height (pixels).
 *  @param  mrefresh  Mode refresh rate (Hz).
 *  @param  mbpp      Mode colour depth (bits per pixel).
 */
#define init_mode(idx, mwidth, mheight, mrefresh, mbpp)                                                    \
{                                                                                                          \
  dlo_mode_data[idx].width      = mwidth;                                                                  \
  dlo_mode_data[idx].height     = mheight;                                                                 \
  dlo_mode_data[idx].refresh    = mrefresh;                                                                \
  dlo_mode_data[idx].bpp        = mbpp;                                                                    \
  dlo_mode_data[idx].data       = DLO_MODE_DATA_##mwidth##_##mheight##_##mrefresh##_##mbpp##_0;            \
  dlo_mode_data[idx].data_sz    = DSIZEOF(DLO_MODE_DATA_##mwidth##_##mheight##_##mrefresh##_##mbpp##_0);   \
  dlo_mode_data[idx].mode_en    = DLO_MODE_ENABLE_##mwidth##_##mheight##_##mrefresh##_##mbpp##_0;          \
  dlo_mode_data[idx].mode_en_sz = DSIZEOF(DLO_MODE_ENABLE_##mwidth##_##mheight##_##mrefresh##_##mbpp##_0); \
  dlo_mode_data[idx].low_blank  = false;                                                                   \
}


/* External-scope inline functions -----------------------------------------------------*/


/** Swap the endianness of a long (four byte) integer.
 *
 *  @param  val  Integer to alter.
 *
 *  @return  Value with swapped endianness.
 */
inline uint32_t swap_endian_l(uint32_t val)
{
  return ((val & 0xFF) << 24) | ((val & 0xFF00) << 8) | ((val & 0xFF0000) >> 8) | (( val >> 24) & 0xFF);
}


/** Swap the endianness of a short (two byte) integer.
 *
 *  @param  val  Integer to alter.
 *
 *  @return  Value with swapped endianness.
 */
inline uint16_t swap_endian_s(uint16_t val)
{
  return ((val >> 8) & 0xFF) | ((val << 8) & 0xFF00);
}


extern uint32_t swap_endian_l(uint32_t val);
extern uint16_t swap_endian_s(uint16_t val);


/* File-scope types --------------------------------------------------------------------*/


/** Structure to hold information about a specific screen mode.
 */
typedef struct dlo_mode_data_s
{
  uint16_t width;        /**< Width (pixels). */
  uint16_t height;       /**< Height (pixels). */
  uint8_t  refresh;      /**< Refresh rate (Hz). */
  uint8_t  bpp;          /**< Colour depth (bits per pixel). */
  char    *data;         /**< Block of mode data. */
  size_t   data_sz;      /**< Size of mode data block (bytes). */
  char    *mode_en;      /**< Block of mode enable data. */
  size_t   mode_en_sz;   /**< Size of mode enable data block (bytes). */
  bool     low_blank;    /**< Screen mode has reduced blanking. */
} dlo_mode_data_t;       /**< A struct @a dlo_mode_data_s. */


/** Established timing information, derived from EDID.
 */
typedef struct est_timing_s
{
  uint16_t width;        /**< Width of mode (pixels). */
  uint16_t height;       /**< Height of mode (pixels). */
  uint8_t  refresh;      /**< Mode refresh rate (Hz). */
} est_timing_t;          /**< A struct @a est_timings_s. */


/** Vendor/product information.
 */
typedef struct edid_prod_id_s
{
  uint16_t manuf_name;   /**< ID manufacturer code. */
  uint16_t prod_code;    /**< ID product code. */
  uint32_t serial_num;   /**< ID serial number. */
  uint8_t  manuf_wk;     /**< Week of manufacture. */
  uint8_t  manuf_yr;     /**< Year of manufacture. */
} edid_prod_id_t;        /**< A struct @a edid_prod_id_s. */


/** EDID structure information.
 */
typedef struct edid_struct_vsn_s
{
  uint8_t number;        /**< Version number. */
  uint8_t revision;      /**< Revision number. */
} edid_struct_vsn_t;     /**< A struct @a edid_struct_vsn_s. */


/** Basic dislpay parameters/features.
 */
typedef struct edid_basic_params_s
{
  uint8_t input_def;     /**< Video input definition. */
  uint8_t max_horiz_sz;  /**< Maximum horizontal image size (cm). */
  uint8_t max_vert_sz;   /**< Maximum vertical image size (cm). */
  float   gamma;         /**< Display transfer characteristic (gamma). */
  uint8_t features;      /**< Feature support. */
} edid_basic_params_t;   /**< A struct @a edid_basic_params_s. */


/** Colour characteristics.
 */
typedef struct edid_colours_s
{
  uint16_t red_grn_low;  /**< Red/green low bits. */
  uint16_t blu_wht_low;  /**< Blue/white low bits. */
  uint16_t red_x;        /**< Red-x (bits 9-2). */
  uint16_t red_y;        /**< Red-y (bits 9-2). */
  uint16_t grn_x;        /**< Green-x (bits 9-2). */
  uint16_t grn_y;        /**< Green-y (bits 9-2). */
  uint16_t blu_x;        /**< Blue-x (bits 9-2). */
  uint16_t blu_y;        /**< Blue-y (bits 9-2). */
  uint16_t wht_x;        /**< White-x (bits 9-2). */
  uint16_t wht_y;        /**< White-y (bits 9-2). */
} edid_colours_t;        /**< A struct @a edid_colours_s. */


/** Established timings.
 */
typedef struct edid_est_timings_s
{
  uint8_t timings[2];    /**< Bitfields of established timings. */
  uint8_t resvd;         /**< Manufacturer's reserved timings. */
} edid_est_timings_t;    /**< A struct @a edid_est_timings_s. */


/** Standard timing identification.
 */
typedef struct edid_std_timing_s
{
  uint16_t timing_id[8];  /**< Standard timing identification. */
} edid_std_timing_t;      /**< A struct @a edid_std_timing_s. */


/** Detailed timing description.
 */
typedef struct edid_detail_s
{
  bool     is_detail;        /**< Flag is set to true - the structure is an @a edid_detail_t. */
  float    pixclk;           /**< Timing parameter. */
  uint32_t hactl;            /**< Timing parameter. */
  uint32_t hblankl;          /**< Timing parameter. */
  uint32_t hactblankh;       /**< Timing parameter. */
  uint32_t vactl;            /**< Timing parameter. */
  uint32_t vblankl;          /**< Timing parameter. */
  uint32_t vactblankh;       /**< Timing parameter. */
  uint32_t hsyncoffl;        /**< Timing parameter. */
  uint32_t hsyncwl;          /**< Timing parameter. */
  uint32_t vsyncoffvsyncwl;  /**< Timing parameter. */
  uint32_t synch;            /**< Timing parameter. */
  uint32_t hsizel;           /**< Timing parameter. */
  uint32_t vsizel;           /**< Timing parameter. */
  uint32_t hvsizeh;          /**< Timing parameter. */
  uint8_t  hbord;            /**< Timing parameter. */
  uint8_t  vbord;            /**< Timing parameter. */
  uint8_t  flags;            /**< Timing parameter. */
} edid_detail_t;             /**< A struct @a edid_detail_s. */


/** Monitor descriptor.
 */
typedef struct edid_monitor_desc_s
{
  bool    is_detail;     /**< Flag is set to false - the structure is not an @a edid_detail_t. */
  uint8_t unknown[18];   /**< Contents of block are unknown. */
} edid_monitor_desc_t;   /**< A struct @a edid_monitor_desc_s. */


/** A timing description block may be either a @a edid_detail_t or a @a edid_monitor_desc_t.
 */
typedef union edid_timing_desc_u
{
  edid_detail_t       detail;  /**< Either a detailed timing description. */
  edid_monitor_desc_t desc;    /**< Or a monitor descriptor. */
} edid_timing_desc_t;          /**< A struct @a edid_timing_desc_s. */


/** An EDID extension block.
 */
typedef struct edid_ext_block_s
{
  uint8_t unknown[128];  /**< Contents of block are unknown. */
} edid_ext_block_t;      /**< A struct @a edid_ext_block_s. */


/** An EDID structure.
 */
typedef struct edid_format_s
{
  edid_prod_id_t      product;      /**< Vendor/product information. */
  edid_struct_vsn_t   version;      /**< EDID structure information. */
  edid_basic_params_t basic;        /**< Basic dislpay parameters/features. */
  edid_colours_t      colours;      /**< Colour characteristics. */
  edid_est_timings_t  est_timings;  /**< Established timings. */
  edid_std_timing_t   std_timings;  /**< Standard timing identification. */
  edid_timing_desc_t  timings[4];   /**< Timing descriptions. */
  uint8_t             ext_blocks;   /**< Number of extension blocks. */
} edid_format_t;                    /**< A struct @a edid_format_s. */


/* External scope variables ------------------------------------------------------------*/


/* File-scope variables ----------------------------------------------------------------*/


/** Array of hard-wired screen mode definitions.
 */
static dlo_mode_data_t dlo_mode_data[DLO_MODE_DATA_NUM];


/** Mode information corresponding with flag bits in EDID establisted timings bytes.
 */
const est_timing_t est_timings[24] =
{
  { 800, 600, 60 },        /* bit 0 */
  { 800, 600, 56 },        /* bit 1 */
  { 640, 480, 75 },        /* bit 2 */
  { 640, 480, 72 },        /* bit 3 */
  { 640, 480, 67 },        /* bit 4 */
  { 640, 480, 60 },        /* bit 5 */
  { 720, 400, 88 },        /* bit 6 */
  { 720, 400, 70 },        /* bit 7 */
  { 1280, 1024, 75 },      /* bit 8 */
  { 1024, 768, 75 },       /* bit 9 */
  { 1024, 768, 70 },       /* bit 10 */
  { 1024, 768, 60 },       /* bit 11 */
  { 1024, 768, 87 },       /* bit 12 */
  { 832, 624, 75 },        /* bit 13 */
  { 800, 600, 75 },        /* bit 14 */
  { 800, 600, 72 },        /* bit 15 */
  { 0, 0, 0 },             /* bit 16 */
  { 0, 0, 0 },             /* bit 17 */
  { 0, 0, 0 },             /* bit 18 */
  { 0, 0, 0 },             /* bit 19 */
  { 0, 0, 0 },             /* bit 20 */
  { 0, 0, 0 },             /* bit 21 */
  { 0, 0, 0 },             /* bit 22 */
  { 1152, 870, 75 }        /* bit 23 */
};


/* File-scope function declarations ----------------------------------------------------*/


/** Append a video register setting command onto the specified char block.
 *
 *  @param  dev  Pointer to @a dlo_device_t structure.
 *  @param  reg  Register number (0..255).
 *  @param  val  Value to set (byte).
 *
 *  @return  Return code, zero for no error.
 */
static dlo_retcode_t vreg(dlo_device_t * const dev, uint8_t reg, uint8_t val);


/** Append a video register setting command onto the specified char block.
 *
 *  @param  dev  Pointer to @a dlo_device_t structure.
 *  @param  reg  Register number (0..255).
 *  @param  val  Value to set (byte).
 *
 *  @return  Return code, zero for no error.
 */
static dlo_retcode_t vbuf(dlo_device_t * const dev, const char * const buf, size_t len);


/** Check an EDID checksum to see if it is invalid.
 *
 *  @param  ptr   Pointer to base of EDID structure data.
 *  @param  size  Size of data (bytes).
 *
 *  @return  true if checksum is incorrect, false if OK.
 */
static bool bad_edid_checksum(const uint8_t * const ptr, const size_t size);


/** Parse an EDID detailed timing descriptor/mode descriptor.
 *
 *  @param  desc  Pointer to descriptor structure to initialise.
 *  @param  ptr   Pointer to base of descriptor in EDID data to parse.
 */
static void parse_edid_detail_desc(edid_timing_desc_t * const desc, const uint8_t * const ptr);


/** Parse EDID colour characteristics.
 *
 *  @param  cols  Pointer to colour structure to initialise.
 *  @param  ptr   Pointer to base of colour characteristics block to parse from EDID data.
 */
static void parse_edid_colours(edid_colours_t * const cols, const uint8_t * const ptr);


/** Look for a mode definition amongst the list of known modes.
 *
 *  @param  dev      Pointer to @a dlo_device_t structure.
 *  @param  width    Width of desired display (pixels).
 *  @param  height   Hieght of desired display (pixels) - zero to select first available.
 *  @param  refresh  Desired refresh rate (Hz) - zero to select first available.
 *  @param  bpp      Colour depth (bits per pixel) - zero to select first available.
 *
 *  @return  Mode number of the best matching mode definition (or @a DLO_INVALID_MODE if none found).
 */
static dlo_modenum_t get_mode_number(dlo_device_t * const dev, const uint16_t width, const uint16_t height, const uint8_t refresh, const uint8_t bpp);


/** Given a bitmap and a mode number, set the current screen mode.
 *
 *  @param  bmp   Bitmap to set as the current rastered display mode.
 *  @param  mode  Mode number for best match to the supplied bitmap.
 *
 *  @return  Return code, zero for no error.
 */
static dlo_retcode_t mode_select(dlo_device_t * const dev, const dlo_mode_t * const desc, const dlo_modenum_t mode);


/** Look up the specified mode and add to the supported list if found.
 *
 *  @param  dev      Pointer to @a dlo_device_t structure.
 *  @param  idx      Index - incrememnted if the mode was found.
 *  @param  width    Width of the mode to look for (pixels).
 *  @param  height   Height of the mode to look for (pixels).
 *  @param  refresh  Refresh rate of the mode to look for (Hz).
 *
 *  @return  Updated index (as @a idx if mode not found, else incremented by one).
 */
static uint16_t add_supported(dlo_device_t * const dev, uint16_t idx, const uint16_t width, const uint16_t height, const uint8_t refresh);


/** Build a list of supported modes, based upon the supplied EDID information.
 *
 *  @param  dev   Device to update.
 *  @param  edid  Pointer to structure holding parsed EDID information.
 *
 *  @return  Return code, zero for no error.
 */
static dlo_retcode_t lookup_edid_modes(dlo_device_t * const dev, const edid_format_t * const edid);


/** Program the base addresses of the video display in the device.
 *
 *  @param  dev    Pointer to @a dlo_device_t structure.
 *  @param  base   Address of the base of the 16 bpp segment.
 *  @param  base8  Address of the base of the 8 bpp segment.
 *
 *  @return  Return code, zero for no error.
 *
 *  Note: this call first will cause any buffered commands to be sent to the device then
 *  the set base commands are sent. The buffer to that device is thus flushed.
 */
static dlo_retcode_t set_base(dlo_device_t * const dev, const dlo_ptr_t base, const dlo_ptr_t base8);


/* Public function definitions ---------------------------------------------------------*/


dlo_retcode_t dlo_mode_init(const dlo_init_t flags)
{
  init_mode(0,  1920, 1080, 60, 24);
  init_mode(1,  1600, 1200, 60, 24);
  init_mode(2,  1400, 1050, 85, 24);
  init_mode(3,  1400, 1050, 75, 24);
  init_mode(4,  1400, 1050, 60, 24);
  init_mode(5,  1400, 1050, 60, 24);
  init_mode(6,  1366, 768,  60, 24);
  init_mode(7,  1360, 768,  60, 24);
  init_mode(8,  1280, 960,  85, 24);
  init_mode(9,  1280, 960,  60, 24);
  init_mode(10, 1280, 800,  60, 24);
  init_mode(11, 1280, 768,  85, 24);
  init_mode(12, 1280, 768,  75, 24);
  init_mode(13, 1280, 1024, 85, 24);
  init_mode(14, 1280, 1024, 75, 24);
  init_mode(15, 1280, 1024, 60, 24);
  init_mode(16, 1280, 768,  60, 24);
  init_mode(17, 1152, 864,  75, 24);
  init_mode(18, 1024, 768,  85, 24);
  init_mode(19, 1024, 768,  75, 24);
  init_mode(20, 1024, 768,  70, 24);
  init_mode(21, 1024, 768,  60, 24);
  init_mode(22, 848,  480,  60, 24);
  init_mode(23, 800,  600,  85, 24);
  init_mode(24, 800,  600,  75, 24);
  init_mode(25, 800,  600,  72, 24);
  init_mode(26, 800,  600,  60, 24);
  init_mode(27, 800,  600,  56, 24);
  init_mode(28, 800,  480,  60, 24);
  init_mode(29, 720,  400,  85, 24);
  init_mode(30, 720,  400,  70, 24);
  init_mode(31, 640,  480,  85, 24);
  init_mode(32, 640,  480,  75, 24);
  init_mode(33, 640,  480,  73, 24);
  init_mode(34, 640,  480,  60, 24);

  return dlo_ok;
}


dlo_retcode_t dlo_mode_final(const dlo_final_t flags)
{
  return dlo_ok;
}


dlo_mode_t *dlo_mode_from_number(const dlo_modenum_t num)
{
  static dlo_mode_t mode;

  if (num < DLO_MODE_DATA_NUM)
  {
    mode.view.width  = dlo_mode_data[num].width;
    mode.view.height = dlo_mode_data[num].height;
    mode.view.bpp    = dlo_mode_data[num].bpp;
    mode.view.base   = 0;
    mode.refresh     = dlo_mode_data[num].refresh;
    return &mode;
  }
  return NULL;
}


dlo_modenum_t dlo_mode_lookup(dlo_device_t * const dev, const uint16_t width, const uint16_t height, const uint8_t refresh, uint8_t bpp)
{
  /* Check that the requested screen mode is supported */
  //DPRINTF("mode: lookup: %ux%u @ %u Hz, %u bpp\n", width, height, refresh, bpp);
  return bpp != 24 ? DLO_INVALID_MODE : get_mode_number(dev, width, height, refresh, bpp);
}


dlo_retcode_t dlo_mode_change(dlo_device_t * const dev, const dlo_mode_t * const desc, dlo_modenum_t mode)
{
  /* If no mode number was specified on entry, try looking one up for the supplied bitmap */
  if (mode == DLO_INVALID_MODE)
    mode = get_mode_number(dev, desc->view.width, desc->view.height, 0, desc->view.bpp);

  /* Change mode or return an error */
  return mode_select(dev, desc, mode);
}


dlo_retcode_t dlo_mode_parse_edid(dlo_device_t * const dev, const uint8_t * const ptr, const size_t size)
{
  static edid_format_t edid;
  uint32_t             i;

  /* Copy the header bytes straight into our structure (assumes the structure is correct size!) */
  ASSERT(EDID_STRUCT_SZ == size);
  if (memcmp(header, ptr, sizeof(header)))
    return dlo_err_edid_fail;

  if (bad_edid_checksum(ptr, size))
    return dlo_err_edid_fail;

#if (defined(DEBUG) && 0) /* If you really want this verbose debug output, change the 0 to a 1 */
  uint32_t j;
  /* Parse the vendor/product information */
  for (j = 0; j < 128; j += 8)
  {
    DPRINTF("mode: edid: raw: ");
    for (i = 0; i < 8; i++)
      DPRINTF("%02X=%02X ", i + j, RD_B(ptr + i + j));
    DPRINTF("\n");
  }
#endif

  edid.product.manuf_name = LETOHS(RD_S(ptr + 0x08));
  edid.product.prod_code  = LETOHS(RD_S(ptr + 0x0A));
  edid.product.serial_num = LETOHL(RD_L(ptr + 0x0C));
  edid.product.manuf_wk   = RD_B(ptr + 0x10);
  edid.product.manuf_yr   = RD_B(ptr + 0x11);
  //DPRINTF("mode: edid: manuf &%X prod &%X serial &%X wk %u yr %u\n",
  //        edid.product.manuf_name, edid.product.prod_code, edid.product.serial_num,
  //        edid.product.manuf_wk, edid.product.manuf_yr + 1990);

  /* Parse the EDID structure information */
  edid.version.number   = RD_B(ptr + 0x12);
  edid.version.revision = RD_B(ptr + 0x13);
  //DPRINTF("edid: version &%02X revision &%02X\n", edid.version.number, edid.version.revision);

  /* Parse the basic dislpay parameters/features */
  edid.basic.input_def    = RD_B(ptr + 0x14);
  edid.basic.max_horiz_sz = RD_B(ptr + 0x15);
  edid.basic.max_vert_sz  = RD_B(ptr + 0x16);
  edid.basic.gamma        = (100.0 + RD_B(ptr + 0x17)) / 100.0;
  edid.basic.features     = RD_B(ptr + 0x18);

  /* Parse the colour characteristics */
  parse_edid_colours(&(edid.colours), ptr + 0x19);

  /* Parse the established timings */
  edid.est_timings.timings[0] = RD_B(ptr + 0x23);
  edid.est_timings.timings[1] = RD_B(ptr + 0x24);
  edid.est_timings.resvd      = RD_B(ptr + 0x25);

  /* Parse the bits at the end of the EDID structure */
  edid.ext_blocks = RD_B(ptr + 0x7E);

  /* Parse all of the standard timing identification */
  for (i = 0; i < sizeof(edid.std_timings.timing_id); i++)
    edid.std_timings.timing_id[i] = RD_B(ptr + 0x26 + i);

  /* Parse all of the detailed timing descriptions (or monitor descriptors) */
  for (i = 0; i < 4; i++)
    parse_edid_detail_desc(&(edid.timings[i]), ptr + 0x36 + (i * 0x12));

  return lookup_edid_modes(dev, &edid);
}


void use_default_modes(dlo_device_t * const dev)
{
  uint32_t i;

  for (i = 0; i < DLO_MODE_DATA_NUM; i++)
    dev->supported[i] = (dlo_modenum_t)i;
}


/* File-scope function definitions -----------------------------------------------------*/


static dlo_retcode_t vreg(dlo_device_t * const dev, uint8_t reg, uint8_t val)
{
  if (dev->bufend - dev->bufptr < 4)
    return dlo_err_buf_full;

  *(dev->bufptr)++ = '\xAF';
  *(dev->bufptr)++ = '\x20';
  *(dev->bufptr)++ = reg;
  *(dev->bufptr)++ = val;

  return dlo_ok;
}


static dlo_retcode_t vbuf(dlo_device_t * const dev, const char * buf, size_t len)
{
  if (dev->bufend - dev->bufptr < len)
    return dlo_err_buf_full;

  while (len--)
    *(dev->bufptr)++ = *buf++;

  return dlo_ok;
}


static dlo_modenum_t get_mode_number(dlo_device_t * const dev, const uint16_t width, const uint16_t height, const uint8_t refresh, const uint8_t bpp)
{
  dlo_modenum_t idx;
  uint32_t       num;

  /* Look for all matching modes in our array of video timing structures.
   *
   * Note: if we don't have EDID data for the monitor attached to the device
   * we simply look through all the modes we have, rather than only looking
   * at the supported modes list.
   */
  for (idx = 0; idx < DLO_MODE_DATA_NUM; idx++)
  {
    /* Read the mode number from the device's supported modes array */
    num = dev->supported[idx];
    if (num == DLO_INVALID_MODE)
      break;

    /* This sequence of if statements looks odd, take care if you decide to 'optimise' it! */
    if (dlo_mode_data[num].width != width)
      continue;
    if (bpp && dlo_mode_data[num].bpp != bpp)
      continue;
    if (height && dlo_mode_data[num].height != height)
      continue;
    if (refresh && dlo_mode_data[num].refresh != refresh)
      continue;

    /* If we're satisfied with the tests above, then return the mode number */
    return num;
  }

  /* No matches found, return an invalid mode number */
  return DLO_INVALID_MODE;
}


static dlo_retcode_t mode_select(dlo_device_t * const dev, const dlo_mode_t * const desc, const dlo_modenum_t mode)
{
  if (mode >= DLO_MODE_DATA_NUM || mode == DLO_INVALID_MODE)
    return dlo_err_bad_mode;

  /* Base address must be aligned to a two byte boundary */
  if (desc->view.base & 1)
    return dlo_err_bad_mode;

  /* Flush the command buffer */
  if (dlo_ok != dlo_usb_write(dev))
    return DLO_INVALID_MODE;

  dev->mode.view.base = desc->view.base;
  dev->base8          = desc->view.base + (BYTES_PER_16BPP * desc->view.width * desc->view.height);
  ERR(set_base(dev, dev->mode.view.base, dev->base8));

  /* Only change mode if the new raster bitmap's characteristics differ from the current.
   *
   * Note: don't compare reduced blanking flag because if the rest is the same, we can use the
   * same blanking type. However, there's an outside chance that the low_blank hint was changed
   * since entering the current mode in which case you may well want a mode change to happen but
   * we'll just hope that never happens (seems like a very unlikely scenario).
   */
  if (desc->view.width  != dev->mode.view.width  ||
      desc->view.height != dev->mode.view.height ||
      desc->view.bpp    != dev->mode.view.bpp)
  {
    ERR(dlo_usb_chan_sel(dev, dlo_mode_data[mode].mode_en, dlo_mode_data[mode].mode_en_sz));
    ERR(dlo_usb_write_buf(dev, dlo_mode_data[mode].data, dlo_mode_data[mode].data_sz));
    ERR(dlo_usb_chan_sel(dev, DLO_MODE_POSTAMBLE, DSIZEOF(DLO_MODE_POSTAMBLE)));
  }

  /* Update the device with the new mode details */
  dev->mode         = *desc;
  dev->mode.refresh = dlo_mode_data[mode].refresh;
  dev->low_blank    = dlo_mode_data[mode].low_blank;

  //DPRINTF("mode: select: %ux%u @ %u Hz %u bpp (base &%X base8 &%X low? %s)\n",
  //        dev->mode.view.width, dev->mode.view.height, dev->mode.refresh, dev->mode.view.bpp,
  //        dev->mode.view.base, dev->base8, dev->low_blank ? "yes" : "no");

  /* Flush the command buffer */
  ERR(dlo_usb_write(dev));

  /* Return a warning for DL160 modes */
  return (mode < DLO_DL120_MODES) ? dlo_warn_dl160_mode : dlo_ok;
}


static uint16_t add_supported(dlo_device_t * const dev, uint16_t idx, const uint16_t width, const uint16_t height, const uint8_t refresh)
{
  dlo_modenum_t num;

  num = get_mode_number(dev, width, height, refresh, 24);
  if (num != DLO_INVALID_MODE)
    dev->supported[idx++] = num;

//  DPRINTF("mode: add_supt: %ux%u @ %u Hz, %u bpp = num %u\n", width, height, refresh, 24, (int)num);

  return idx;
}


static dlo_retcode_t lookup_edid_modes(dlo_device_t * const dev, const edid_format_t * const edid)
{
  uint32_t timings, bit;
  uint32_t idx    = 0;

  /* Clear the native mode information for the device */
  (void) dlo_memset(&dev->native, 0, sizeof(dlo_mode_t));

  /* Add mode numbers for any supported established timing modes */
  timings = edid->est_timings.timings[0] | (edid->est_timings.timings[1] << 8) | (edid->est_timings.resvd << 16);
  for (bit = 0; bit < 24; bit++)
  {
    if (est_timings[bit].width)
      idx = add_supported(dev, idx, est_timings[bit].width, est_timings[bit].height, est_timings[bit].refresh);
  }

  /* Add further support from the detailed timing descriptions */
  for (timings = 0; timings < 4; timings++)
  {
    const edid_detail_t * const detail = &(edid->timings[timings].detail);

    if (detail->is_detail)
    {
      uint8_t hz;

      for (hz = 50; hz < 100; hz++)
      {
        uint32_t prev   = idx;
        uint32_t width  = detail->hactl + ((detail->hactblankh & 0xF0) << 4);
        uint32_t height = detail->vactl + ((detail->vactblankh & 0xF0) << 4);

        idx = add_supported(dev, idx, width, height, hz);

        /* Have we added a mode definition for the native resolution reported by the display? */
        if (idx != prev)
        {
          dlo_modenum_t num = dev->supported[prev];

          dev->native.view.base   = 0;
          dev->native.view.width  = dlo_mode_data[num].width;
          dev->native.view.height = dlo_mode_data[num].height;
          dev->native.view.bpp    = dlo_mode_data[num].bpp;
          dev->native.refresh     = dlo_mode_data[num].refresh;
          DPRINTF("mode: lookup: native mode %u (%ux%u @ %u bpp, %uHz base &%X)\n",
                  num,
                  dev->native.view.width,
                  dev->native.view.height,
                  dev->native.view.bpp,
                  dev->native.refresh,
                  (int)dev->native.view.base);
        }
      }
    }
  }

  /* Fill any remaining array entries with the invalid mode constant */
  while (idx < DLO_MODE_DATA_NUM - 1)
    dev->supported[idx++] = DLO_INVALID_MODE;

  return dlo_ok;
}


static dlo_retcode_t set_base(dlo_device_t * const dev, const dlo_ptr_t base, const dlo_ptr_t base8)
{
  //DPRINTF("mode: set_base: base=&%X base8=&%X\n", base, base8);
  ERR(vbuf(dev, WRITE_VIDREG_LOCK, DSIZEOF(WRITE_VIDREG_LOCK)));
  ERR(vreg(dev, 0x20, base >> 16));
  ERR(vreg(dev, 0x21, base >> 8));
  ERR(vreg(dev, 0x22, base));
  ERR(vreg(dev, 0x26, base8 >> 16));
  ERR(vreg(dev, 0x27, base8 >> 8));
  ERR(vreg(dev, 0x28, base8));
  ERR(vbuf(dev, WRITE_VIDREG_UNLOCK, DSIZEOF(WRITE_VIDREG_UNLOCK)));
  ERR(dlo_usb_write(dev));

  return dlo_ok;
}


static bool bad_edid_checksum(const uint8_t * const ptr, const size_t size)
{
  uint32_t i;
  uint8_t  csum = 0;

  for (i = 0; i < size; i++)
    csum += ptr[i];

  return 0 != csum;
}


static void parse_edid_detail_desc(edid_timing_desc_t * const desc, const uint8_t * const ptr)
{
  if (RD_B(ptr) || RD_B(ptr + 1) || RD_B(ptr + 2))
  {
    //DPRINTF("edid: found timing detail (&%04X)\n", LETOHS(RD_S(ptr)));
    desc->detail.is_detail       = true;
    desc->detail.pixclk          = (float)LETOHS(RD_S(ptr)) / 100.0;
    desc->detail.hactl           = RD_B(ptr + 0x02);
    desc->detail.hblankl         = RD_B(ptr + 0x03);
    desc->detail.hactblankh      = RD_B(ptr + 0x04);
    desc->detail.vactl           = RD_B(ptr + 0x05);
    desc->detail.vblankl         = RD_B(ptr + 0x06);
    desc->detail.vactblankh      = RD_B(ptr + 0x07);
    desc->detail.hsyncoffl       = RD_B(ptr + 0x08);
    desc->detail.hsyncwl         = RD_B(ptr + 0x09);
    desc->detail.vsyncoffvsyncwl = RD_B(ptr + 0x0A);
    desc->detail.synch           = RD_B(ptr + 0x0B);
    desc->detail.hsizel          = RD_B(ptr + 0x0C);
    desc->detail.vsizel          = RD_B(ptr + 0x0D);
    desc->detail.hvsizeh         = RD_B(ptr + 0x0E);
    desc->detail.hbord           = RD_B(ptr + 0x0F);
    desc->detail.vbord           = RD_B(ptr + 0x10);
    desc->detail.flags           = RD_B(ptr + 0x11);

    //DPRINTF("edid: Pixel Clock:     %f MHz\n", (float)desc->detail.pixclk);
    //DPRINTF("edid: H Active pixels: %d\n",     desc->detail.hactl + ((desc->detail.hactblankh & 0xF0) << 4));
    //DPRINTF("edid: H Blank pixels:  %d\n",     desc->detail.hblankl + ((desc->detail.hactblankh & 0x0F) << 8));
    //DPRINTF("edid: V Active pixels: %d\n",     desc->detail.vactl + ((desc->detail.vactblankh & 0xF0) << 4));
    //DPRINTF("edid: V Blank pixels:  %d\n",     desc->detail.vblankl + ((desc->detail.vactblankh & 0x0F) << 8));
    //DPRINTF("edid: H Sync Off:      %d\n",     desc->detail.hsyncoffl + ((desc->detail.synch & 0xC0) << 2));
    //DPRINTF("edid: H Sync Width:    %d\n",     desc->detail.hsyncwl + ((desc->detail.synch & 0x30) << 4));
    //DPRINTF("edid: V Sync Off:      %d\n",     (desc->detail.vsyncoffvsyncwl >> 4) + ((desc->detail.synch & 0x0C) << 6));
    //DPRINTF("edid: V Sync Width:    %d\n",     (desc->detail.vsyncoffvsyncwl & 0xF) + ((desc->detail.synch & 0x03) << 8));
    //DPRINTF("edid: H size:          %d mm\n",  desc->detail.hsizel + ((desc->detail.hvsizeh & 0xF0) << 4));
    //DPRINTF("edid: V size:          %d mm\n",  desc->detail.vsizel + ((desc->detail.hvsizeh & 0x0F) << 8));
  }
  else
  {
    /* It's a mode descriptor - ignore for now */
    //DPRINTF("edid: found a mode descriptor type &%02X\n", RD_B(ptr+3));
    desc->desc.is_detail = false;
    switch (RD_B(ptr+3))
    {
      case 0xFC:
      case 0xFE:
      case 0xFF:
      {
        char  buf[14];
        char *chr;

        snprintf(buf, 13, "%s", ptr + 5);
        buf[13] = '\0';
        chr = strchr(buf, '\n');
        if (chr)
          *chr = '\0';
        //DPRINTF("edid: monitor string '%s'\n", buf);
        break;
      }
      default:
      {
        //uint32_t i;
        //DPRINTF("edid: ");
        //for (i = 0; i < 0x12; i++)
        //  DPRINTF("%02X ", RD_B(ptr+i));
        //DPRINTF("\n");
      }
    }
  }
}

static void parse_edid_colours(edid_colours_t * const cols, const uint8_t * const ptr)
{
  /* Read the raw data */
  cols->red_grn_low = RD_B(ptr + 0x19);
  cols->blu_wht_low = RD_B(ptr + 0x1A);
  cols->red_x       = RD_B(ptr + 0x1B);
  cols->red_y       = RD_B(ptr + 0x1C);
  cols->grn_x       = RD_B(ptr + 0x1D);
  cols->grn_y       = RD_B(ptr + 0x1E);
  cols->blu_x       = RD_B(ptr + 0x1F);
  cols->blu_y       = RD_B(ptr + 0x20);
  cols->wht_x       = RD_B(ptr + 0x21);
  cols->wht_y       = RD_B(ptr + 0x22);

  /* Do the expansion */
  cols->red_x = ((cols->red_grn_low & 0xC0) >> 6) + (cols->red_x << 2);
  cols->red_y = ((cols->red_grn_low & 0x30) >> 4) + (cols->red_y << 2);
  cols->grn_x = ((cols->red_grn_low & 0x0C) >> 2) + (cols->grn_x << 2);
  cols->grn_y = ((cols->red_grn_low & 0x03) >> 0) + (cols->grn_y << 2);
  cols->blu_x = ((cols->blu_wht_low & 0xC0) >> 6) + (cols->blu_x << 2);
  cols->blu_y = ((cols->blu_wht_low & 0x40) >> 4) + (cols->blu_y << 2);
  cols->wht_x = ((cols->blu_wht_low & 0x0C) >> 2) + (cols->wht_y << 2);
  cols->wht_y = ((cols->blu_wht_low & 0x03) >> 0) + (cols->wht_y << 2);
}


/* End of file -------------------------------------------------------------------------*/
