/*
 * Mesa 3-D graphics library
 * Version:  7.9
 *
 * Copyright (C) 2010 LunarG Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * Authors:
 *    Chia-I Wu <olv@lunarg.com>
 */

#include "util/u_memory.h"
#include "util/u_inlines.h"
#include "egllog.h"

#include "native_drm.h"

static boolean
drm_surface_validate(struct native_surface *nsurf, uint attachment_mask,
                     unsigned int *seq_num, struct pipe_resource **textures,
                     int *width, int *height)
{
   struct drm_surface *drmsurf = drm_surface(nsurf);

   if (!resource_surface_add_resources(drmsurf->rsurf, attachment_mask))
      return FALSE;
   if (textures)
      resource_surface_get_resources(drmsurf->rsurf, textures, attachment_mask);

   if (seq_num)
      *seq_num = drmsurf->sequence_number;
   if (width)
      *width = drmsurf->width;
   if (height)
      *height = drmsurf->height;

   return TRUE;
}

/**
 * Add textures as DRM framebuffers.
 */
static boolean
drm_surface_init_framebuffers(struct native_surface *nsurf, boolean need_back)
{
   struct drm_surface *drmsurf = drm_surface(nsurf);
   struct drm_display *drmdpy = drmsurf->drmdpy;
   int num_framebuffers = (need_back) ? 2 : 1;
   int i, err;

   for (i = 0; i < num_framebuffers; i++) {
      struct drm_framebuffer *fb;
      enum native_attachment natt;
      struct winsys_handle whandle;
      uint block_bits;

      if (i == 0) {
         fb = &drmsurf->front_fb;
         natt = NATIVE_ATTACHMENT_FRONT_LEFT;
      }
      else {
         fb = &drmsurf->back_fb;
         natt = NATIVE_ATTACHMENT_BACK_LEFT;
      }

      if (!fb->texture) {
         /* make sure the texture has been allocated */
         resource_surface_add_resources(drmsurf->rsurf, 1 << natt);
         fb->texture =
            resource_surface_get_single_resource(drmsurf->rsurf, natt);
         if (!fb->texture)
            return FALSE;
      }

      /* already initialized */
      if (fb->buffer_id)
         continue;

      /* TODO detect the real value */
      fb->is_passive = TRUE;

      memset(&whandle, 0, sizeof(whandle));
      whandle.type = DRM_API_HANDLE_TYPE_KMS;

      if (!drmdpy->base.screen->resource_get_handle(drmdpy->base.screen,
               fb->texture, &whandle))
         return FALSE;

      block_bits = util_format_get_blocksizebits(drmsurf->color_format);
      err = drmModeAddFB(drmdpy->fd, drmsurf->width, drmsurf->height,
            block_bits, block_bits, whandle.stride, whandle.handle,
            &fb->buffer_id);
      if (err) {
         fb->buffer_id = 0;
         return FALSE;
      }
   }

   return TRUE;
}

static boolean
drm_surface_flush_frontbuffer(struct native_surface *nsurf)
{
#ifdef DRM_MODE_FEATURE_DIRTYFB
   struct drm_surface *drmsurf = drm_surface(nsurf);
   struct drm_display *drmdpy = drmsurf->drmdpy;

   if (drmsurf->front_fb.is_passive)
      drmModeDirtyFB(drmdpy->fd, drmsurf->front_fb.buffer_id, NULL, 0);
#endif

   return TRUE;
}

static boolean
drm_surface_swap_buffers(struct native_surface *nsurf)
{
   struct drm_surface *drmsurf = drm_surface(nsurf);
   struct drm_crtc *drmcrtc = &drmsurf->current_crtc;
   struct drm_display *drmdpy = drmsurf->drmdpy;
   struct drm_framebuffer tmp_fb;
   int err;

   if (!drmsurf->back_fb.buffer_id) {
      if (!drm_surface_init_framebuffers(&drmsurf->base, TRUE))
         return FALSE;
   }

   if (drmsurf->is_shown && drmcrtc->crtc) {
      err = drmModeSetCrtc(drmdpy->fd, drmcrtc->crtc->crtc_id,
            drmsurf->back_fb.buffer_id, drmcrtc->crtc->x, drmcrtc->crtc->y,
            drmcrtc->connectors, drmcrtc->num_connectors, &drmcrtc->crtc->mode);
      if (err)
         return FALSE;
   }

   /* swap the buffers */
   tmp_fb = drmsurf->front_fb;
   drmsurf->front_fb = drmsurf->back_fb;
   drmsurf->back_fb = tmp_fb;

   resource_surface_swap_buffers(drmsurf->rsurf,
         NATIVE_ATTACHMENT_FRONT_LEFT, NATIVE_ATTACHMENT_BACK_LEFT, FALSE);
   /* the front/back textures are swapped */
   drmsurf->sequence_number++;
   drmdpy->event_handler->invalid_surface(&drmdpy->base,
         &drmsurf->base, drmsurf->sequence_number);

   return TRUE;
}

static boolean
drm_surface_present(struct native_surface *nsurf,
                    enum native_attachment natt,
                    boolean preserve,
                    uint swap_interval)
{
   boolean ret;

   if (preserve || swap_interval)
      return FALSE;

   switch (natt) {
   case NATIVE_ATTACHMENT_FRONT_LEFT:
      ret = drm_surface_flush_frontbuffer(nsurf);
      break;
   case NATIVE_ATTACHMENT_BACK_LEFT:
      ret = drm_surface_swap_buffers(nsurf);
      break;
   default:
      ret = FALSE;
      break;
   }

   return ret;
}

static void
drm_surface_wait(struct native_surface *nsurf)
{
   /* no-op */
}

static void
drm_surface_destroy(struct native_surface *nsurf)
{
   struct drm_surface *drmsurf = drm_surface(nsurf);

   if (drmsurf->current_crtc.crtc)
         drmModeFreeCrtc(drmsurf->current_crtc.crtc);

   if (drmsurf->front_fb.buffer_id)
      drmModeRmFB(drmsurf->drmdpy->fd, drmsurf->front_fb.buffer_id);
   pipe_resource_reference(&drmsurf->front_fb.texture, NULL);

   if (drmsurf->back_fb.buffer_id)
      drmModeRmFB(drmsurf->drmdpy->fd, drmsurf->back_fb.buffer_id);
   pipe_resource_reference(&drmsurf->back_fb.texture, NULL);

   resource_surface_destroy(drmsurf->rsurf);
   FREE(drmsurf);
}

static struct drm_surface *
drm_display_create_surface(struct native_display *ndpy,
                           const struct native_config *nconf,
                           uint width, uint height)
{
   struct drm_display *drmdpy = drm_display(ndpy);
   struct drm_config *drmconf = drm_config(nconf);
   struct drm_surface *drmsurf;

   drmsurf = CALLOC_STRUCT(drm_surface);
   if (!drmsurf)
      return NULL;

   drmsurf->drmdpy = drmdpy;
   drmsurf->color_format = drmconf->base.color_format;
   drmsurf->width = width;
   drmsurf->height = height;

   drmsurf->rsurf = resource_surface_create(drmdpy->base.screen,
         drmsurf->color_format,
         PIPE_BIND_RENDER_TARGET |
         PIPE_BIND_SAMPLER_VIEW |
         PIPE_BIND_DISPLAY_TARGET |
         PIPE_BIND_SCANOUT);
   if (!drmsurf->rsurf) {
      FREE(drmsurf);
      return NULL;
   }

   resource_surface_set_size(drmsurf->rsurf, drmsurf->width, drmsurf->height);

   drmsurf->base.destroy = drm_surface_destroy;
   drmsurf->base.present = drm_surface_present;
   drmsurf->base.validate = drm_surface_validate;
   drmsurf->base.wait = drm_surface_wait;

   return drmsurf;
}

/**
 * Choose a CRTC that supports all given connectors.
 */
static uint32_t
drm_display_choose_crtc(struct native_display *ndpy,
                        uint32_t *connectors, int num_connectors)
{
   struct drm_display *drmdpy = drm_display(ndpy);
   int idx;

   for (idx = 0; idx < drmdpy->resources->count_crtcs; idx++) {
      boolean found_crtc = TRUE;
      int i, j;

      for (i = 0; i < num_connectors; i++) {
         drmModeConnectorPtr connector;
         int encoder_idx = -1;

         connector = drmModeGetConnector(drmdpy->fd, connectors[i]);
         if (!connector) {
            found_crtc = FALSE;
            break;
         }

         /* find an encoder the CRTC supports */
         for (j = 0; j < connector->count_encoders; j++) {
            drmModeEncoderPtr encoder =
               drmModeGetEncoder(drmdpy->fd, connector->encoders[j]);
            if (encoder->possible_crtcs & (1 << idx)) {
               encoder_idx = j;
               break;
            }
            drmModeFreeEncoder(encoder);
         }

         drmModeFreeConnector(connector);
         if (encoder_idx < 0) {
            found_crtc = FALSE;
            break;
         }
      }

      if (found_crtc)
         break;
   }

   if (idx >= drmdpy->resources->count_crtcs) {
      _eglLog(_EGL_WARNING,
            "failed to find a CRTC that supports the given %d connectors",
            num_connectors);
      return 0;
   }

   return drmdpy->resources->crtcs[idx];
}

/**
 * Remember the original CRTC status and set the CRTC
 */
static boolean
drm_display_set_crtc(struct native_display *ndpy, int crtc_idx,
                     uint32_t buffer_id, uint32_t x, uint32_t y,
                     uint32_t *connectors, int num_connectors,
                     drmModeModeInfoPtr mode)
{
   struct drm_display *drmdpy = drm_display(ndpy);
   struct drm_crtc *drmcrtc = &drmdpy->saved_crtcs[crtc_idx];
   uint32_t crtc_id;
   int err;

   if (drmcrtc->crtc) {
      crtc_id = drmcrtc->crtc->crtc_id;
   }
   else {
      int count = 0, i;

      /*
       * Choose the CRTC once.  It could be more dynamic, but let's keep it
       * simple for now.
       */
      crtc_id = drm_display_choose_crtc(&drmdpy->base,
            connectors, num_connectors);

      /* save the original CRTC status */
      drmcrtc->crtc = drmModeGetCrtc(drmdpy->fd, crtc_id);
      if (!drmcrtc->crtc)
         return FALSE;

      for (i = 0; i < drmdpy->num_connectors; i++) {
         struct drm_connector *drmconn = &drmdpy->connectors[i];
         drmModeConnectorPtr connector = drmconn->connector;
         drmModeEncoderPtr encoder;

         encoder = drmModeGetEncoder(drmdpy->fd, connector->encoder_id);
         if (encoder) {
            if (encoder->crtc_id == crtc_id) {
               drmcrtc->connectors[count++] = connector->connector_id;
               if (count >= Elements(drmcrtc->connectors))
                  break;
            }
            drmModeFreeEncoder(encoder);
         }
      }

      drmcrtc->num_connectors = count;
   }

   err = drmModeSetCrtc(drmdpy->fd, crtc_id, buffer_id, x, y,
         connectors, num_connectors, mode);
   if (err) {
      drmModeFreeCrtc(drmcrtc->crtc);
      drmcrtc->crtc = NULL;
      drmcrtc->num_connectors = 0;

      return FALSE;
   }

   return TRUE;
}

static boolean
drm_display_program(struct native_display *ndpy, int crtc_idx,
                    struct native_surface *nsurf, uint x, uint y,
                    const struct native_connector **nconns, int num_nconns,
                    const struct native_mode *nmode)
{
   struct drm_display *drmdpy = drm_display(ndpy);
   struct drm_surface *drmsurf = drm_surface(nsurf);
   const struct drm_mode *drmmode = drm_mode(nmode);
   uint32_t connector_ids[32];
   uint32_t buffer_id;
   drmModeModeInfo mode_tmp, *mode;
   int i;

   if (num_nconns > Elements(connector_ids)) {
      _eglLog(_EGL_WARNING, "too many connectors (%d)", num_nconns);
      num_nconns = Elements(connector_ids);
   }

   if (drmsurf) {
      if (!drm_surface_init_framebuffers(&drmsurf->base, FALSE))
         return FALSE;

      buffer_id = drmsurf->front_fb.buffer_id;
      /* the mode argument of drmModeSetCrtc is not constified */
      mode_tmp = drmmode->mode;
      mode = &mode_tmp;
   }
   else {
      /* disable the CRTC */
      buffer_id = 0;
      mode = NULL;
      num_nconns = 0;
   }

   for (i = 0; i < num_nconns; i++) {
      struct drm_connector *drmconn = drm_connector(nconns[i]);
      connector_ids[i] = drmconn->connector->connector_id;
   }

   if (!drm_display_set_crtc(&drmdpy->base, crtc_idx, buffer_id, x, y,
            connector_ids, num_nconns, mode)) {
      _eglLog(_EGL_WARNING, "failed to set CRTC %d", crtc_idx);

      return FALSE;
   }

   if (drmdpy->shown_surfaces[crtc_idx])
      drmdpy->shown_surfaces[crtc_idx]->is_shown = FALSE;
   drmdpy->shown_surfaces[crtc_idx] = drmsurf;

   /* remember the settings for buffer swapping */
   if (drmsurf) {
      uint32_t crtc_id = drmdpy->saved_crtcs[crtc_idx].crtc->crtc_id;
      struct drm_crtc *drmcrtc = &drmsurf->current_crtc;

      if (drmcrtc->crtc)
         drmModeFreeCrtc(drmcrtc->crtc);
      drmcrtc->crtc = drmModeGetCrtc(drmdpy->fd, crtc_id);

      assert(num_nconns < Elements(drmcrtc->connectors));
      memcpy(drmcrtc->connectors, connector_ids,
            sizeof(*connector_ids) * num_nconns);
      drmcrtc->num_connectors = num_nconns;

      drmsurf->is_shown = TRUE;
   }

   return TRUE;
}

static const struct native_mode **
drm_display_get_modes(struct native_display *ndpy,
                      const struct native_connector *nconn,
                      int *num_modes)
{
   struct drm_display *drmdpy = drm_display(ndpy);
   struct drm_connector *drmconn = drm_connector(nconn);
   const struct native_mode **nmodes_return;
   int count, i;

   /* delete old data */
   if (drmconn->connector) {
      drmModeFreeConnector(drmconn->connector);
      FREE(drmconn->drm_modes);

      drmconn->connector = NULL;
      drmconn->drm_modes = NULL;
      drmconn->num_modes = 0;
   }

   /* detect again */
   drmconn->connector = drmModeGetConnector(drmdpy->fd, drmconn->connector_id);
   if (!drmconn->connector)
      return NULL;

   count = drmconn->connector->count_modes;
   drmconn->drm_modes = CALLOC(count, sizeof(*drmconn->drm_modes));
   if (!drmconn->drm_modes) {
      drmModeFreeConnector(drmconn->connector);
      drmconn->connector = NULL;

      return NULL;
   }

   for (i = 0; i < count; i++) {
      struct drm_mode *drmmode = &drmconn->drm_modes[i];
      drmModeModeInfoPtr mode = &drmconn->connector->modes[i];

      drmmode->mode = *mode;

      drmmode->base.desc = drmmode->mode.name;
      drmmode->base.width = drmmode->mode.hdisplay;
      drmmode->base.height = drmmode->mode.vdisplay;
      drmmode->base.refresh_rate = drmmode->mode.vrefresh;
      /* not all kernels have vrefresh = refresh_rate * 1000 */
      if (drmmode->base.refresh_rate < 1000)
         drmmode->base.refresh_rate *= 1000;
   }

   nmodes_return = MALLOC(count * sizeof(*nmodes_return));
   if (nmodes_return) {
      for (i = 0; i < count; i++)
         nmodes_return[i] = &drmconn->drm_modes[i].base;
      if (num_modes)
         *num_modes = count;
   }

   return nmodes_return;
}

static const struct native_connector **
drm_display_get_connectors(struct native_display *ndpy, int *num_connectors,
                           int *num_crtc)
{
   struct drm_display *drmdpy = drm_display(ndpy);
   const struct native_connector **connectors;
   int i;

   if (!drmdpy->connectors) {
      drmdpy->connectors =
         CALLOC(drmdpy->resources->count_connectors, sizeof(*drmdpy->connectors));
      if (!drmdpy->connectors)
         return NULL;

      for (i = 0; i < drmdpy->resources->count_connectors; i++) {
         struct drm_connector *drmconn = &drmdpy->connectors[i];

         drmconn->connector_id = drmdpy->resources->connectors[i];
         /* drmconn->connector is allocated when the modes are asked */
      }

      drmdpy->num_connectors = drmdpy->resources->count_connectors;
   }

   connectors = MALLOC(drmdpy->num_connectors * sizeof(*connectors));
   if (connectors) {
      for (i = 0; i < drmdpy->num_connectors; i++)
         connectors[i] = &drmdpy->connectors[i].base;
      if (num_connectors)
         *num_connectors = drmdpy->num_connectors;
   }

   if (num_crtc)
      *num_crtc = drmdpy->resources->count_crtcs;

   return connectors;
}

static struct native_surface *
drm_display_create_scanout_surface(struct native_display *ndpy,
                                   const struct native_config *nconf,
                                   uint width, uint height)
{
   struct drm_surface *drmsurf;

   drmsurf = drm_display_create_surface(ndpy, nconf, width, height);
   return &drmsurf->base;
}

static struct native_display_modeset drm_display_modeset = {
   .get_connectors = drm_display_get_connectors,
   .get_modes = drm_display_get_modes,
   .create_scanout_surface = drm_display_create_scanout_surface,
   .program = drm_display_program
};

void
drm_display_fini_modeset(struct native_display *ndpy)
{
   struct drm_display *drmdpy = drm_display(ndpy);
   int i;

   if (drmdpy->connectors) {
      for (i = 0; i < drmdpy->num_connectors; i++) {
         struct drm_connector *drmconn = &drmdpy->connectors[i];
         if (drmconn->connector) {
            drmModeFreeConnector(drmconn->connector);
            FREE(drmconn->drm_modes);
         }
      }
      FREE(drmdpy->connectors);
   }

   if (drmdpy->shown_surfaces) {
      FREE(drmdpy->shown_surfaces);
      drmdpy->shown_surfaces = NULL;
   }

   if (drmdpy->saved_crtcs) {
      for (i = 0; i < drmdpy->resources->count_crtcs; i++) {
         struct drm_crtc *drmcrtc = &drmdpy->saved_crtcs[i];

         if (drmcrtc->crtc) {
            /* restore crtc */
            drmModeSetCrtc(drmdpy->fd, drmcrtc->crtc->crtc_id,
                  drmcrtc->crtc->buffer_id, drmcrtc->crtc->x, drmcrtc->crtc->y,
                  drmcrtc->connectors, drmcrtc->num_connectors,
                  &drmcrtc->crtc->mode);

            drmModeFreeCrtc(drmcrtc->crtc);
         }
      }
      FREE(drmdpy->saved_crtcs);
   }

   if (drmdpy->resources) {
      drmModeFreeResources(drmdpy->resources);
      drmdpy->resources = NULL;
   }

   drmdpy->base.modeset = NULL;
}

boolean
drm_display_init_modeset(struct native_display *ndpy)
{
   struct drm_display *drmdpy = drm_display(ndpy);

   /* resources are fixed, unlike crtc, connector, or encoder */
   drmdpy->resources = drmModeGetResources(drmdpy->fd);
   if (!drmdpy->resources) {
      _eglLog(_EGL_DEBUG, "Failed to get KMS resources.  Disable modeset.");
      return FALSE;
   }

   drmdpy->saved_crtcs =
      CALLOC(drmdpy->resources->count_crtcs, sizeof(*drmdpy->saved_crtcs));
   if (!drmdpy->saved_crtcs) {
      drm_display_fini_modeset(&drmdpy->base);
      return FALSE;
   }

   drmdpy->shown_surfaces =
      CALLOC(drmdpy->resources->count_crtcs, sizeof(*drmdpy->shown_surfaces));
   if (!drmdpy->shown_surfaces) {
      drm_display_fini_modeset(&drmdpy->base);
      return FALSE;
   }

   drmdpy->base.modeset = &drm_display_modeset;

   return TRUE;
}
