/*
 * 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>
 */

/**
 * Considering fbdev as an in-kernel window system,
 *
 *  - opening a device opens a connection
 *  - there is only one window: the framebuffer
 *  - fb_var_screeninfo decides window position, size, and even color format
 *  - there is no pixmap
 *
 * Now EGL is built on top of this window system.  So we should have
 *
 *  - the fd as the handle of the native display
 *  - reject all but one native window: NULL
 *  - no pixmap support
 */

#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/fb.h>

#include "pipe/p_screen.h"
#include "util/u_memory.h"
#include "util/u_inlines.h"
#include "util/u_pointer.h"

#include "common/native.h"
#include "common/native_helper.h"
#include "fbdev/fbdev_sw_winsys.h"

struct fbdev_display {
   struct native_display base;

   int fd;
   const struct native_event_handler *event_handler;

   struct fb_fix_screeninfo finfo;
   struct fb_var_screeninfo config_vinfo;
   struct native_config config;

   boolean assume_fixed_vinfo;
};

struct fbdev_surface {
   struct native_surface base;

   struct fbdev_display *fbdpy;
   struct resource_surface *rsurf;
   int width, height;

   unsigned int sequence_number;

   struct fbdev_sw_drawable drawable;
};

static INLINE struct fbdev_display *
fbdev_display(const struct native_display *ndpy)
{
   return (struct fbdev_display *) ndpy;
}

static INLINE struct fbdev_surface *
fbdev_surface(const struct native_surface *nsurf)
{
   return (struct fbdev_surface *) nsurf;
}

static boolean
fbdev_surface_validate(struct native_surface *nsurf, uint attachment_mask,
                     unsigned int *seq_num, struct pipe_resource **textures,
                     int *width, int *height)
{
   struct fbdev_surface *fbsurf = fbdev_surface(nsurf);

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

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

   return TRUE;
}

static enum pipe_format
vinfo_to_format(const struct fb_var_screeninfo *vinfo)
{
   enum pipe_format format = PIPE_FORMAT_NONE;

   /* should also check channel offsets... */
   switch (vinfo->bits_per_pixel) {
   case 32:
      if (vinfo->red.length == 8 &&
          vinfo->green.length == 8 &&
          vinfo->blue.length == 8) {
         format = (vinfo->transp.length == 8) ?
            PIPE_FORMAT_B8G8R8A8_UNORM : PIPE_FORMAT_B8G8R8X8_UNORM;
      }
      break;
   case 16:
      if (vinfo->red.length == 5 &&
          vinfo->green.length == 6 &&
          vinfo->blue.length == 5 &&
          vinfo->transp.length == 0)
         format = PIPE_FORMAT_B5G6R5_UNORM;
      break;
   default:
      break;
   }

   return format;
}

static boolean
fbdev_surface_update_drawable(struct native_surface *nsurf,
                              const struct fb_var_screeninfo *vinfo)
{
   struct fbdev_surface *fbsurf = fbdev_surface(nsurf);
   unsigned x, y, width, height;

   x = vinfo->xoffset;
   y = vinfo->yoffset;
   width = MIN2(vinfo->xres, fbsurf->width);
   height = MIN2(vinfo->yres, fbsurf->height);

   /* sanitize the values */
   if (x + width > vinfo->xres_virtual) {
      if (x > vinfo->xres_virtual)
         width = 0;
      else
         width = vinfo->xres_virtual - x;
   }
   if (y + height > vinfo->yres_virtual) {
      if (y > vinfo->yres_virtual)
         height = 0;
      else
         height = vinfo->yres_virtual - y;
   }

   fbsurf->drawable.format = vinfo_to_format(vinfo);
   fbsurf->drawable.x = vinfo->xoffset;
   fbsurf->drawable.y = vinfo->yoffset;
   fbsurf->drawable.width = vinfo->xres;
   fbsurf->drawable.height = vinfo->yres;

   return (fbsurf->drawable.format != PIPE_FORMAT_NONE &&
           fbsurf->drawable.width &&
           fbsurf->drawable.height);
}

static boolean
fbdev_surface_present(struct native_surface *nsurf,
                      const struct native_present_control *ctrl)
{
   struct fbdev_surface *fbsurf = fbdev_surface(nsurf);
   struct fbdev_display *fbdpy = fbsurf->fbdpy;
   boolean ret = FALSE;

   if (ctrl->swap_interval)
      return FALSE;
   if (ctrl->natt != NATIVE_ATTACHMENT_BACK_LEFT)
      return FALSE;

   if (!fbdpy->assume_fixed_vinfo) {
      struct fb_var_screeninfo vinfo;

      memset(&vinfo, 0, sizeof(vinfo));
      if (ioctl(fbdpy->fd, FBIOGET_VSCREENINFO, &vinfo))
         return FALSE;

      /* present the surface */
      if (fbdev_surface_update_drawable(&fbsurf->base, &vinfo)) {
         ret = resource_surface_present(fbsurf->rsurf,
               ctrl->natt, (void *) &fbsurf->drawable);
      }

      fbsurf->width = vinfo.xres;
      fbsurf->height = vinfo.yres;

      if (resource_surface_set_size(fbsurf->rsurf,
               fbsurf->width, fbsurf->height)) {
         /* surface resized */
         fbsurf->sequence_number++;
         fbdpy->event_handler->invalid_surface(&fbdpy->base,
               &fbsurf->base, fbsurf->sequence_number);
      }
   }
   else {
      /* the drawable never changes */
      ret = resource_surface_present(fbsurf->rsurf,
            ctrl->natt, (void *) &fbsurf->drawable);
   }

   return ret;
}

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

static void
fbdev_surface_destroy(struct native_surface *nsurf)
{
   struct fbdev_surface *fbsurf = fbdev_surface(nsurf);

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

static struct native_surface *
fbdev_display_create_window_surface(struct native_display *ndpy,
                                    EGLNativeWindowType win,
                                    const struct native_config *nconf)
{
   struct fbdev_display *fbdpy = fbdev_display(ndpy);
   struct fbdev_surface *fbsurf;
   struct fb_var_screeninfo vinfo;

   /* there is only one native window: NULL */
   if (win)
      return NULL;

   fbsurf = CALLOC_STRUCT(fbdev_surface);
   if (!fbsurf)
      return NULL;

   fbsurf->fbdpy = fbdpy;

   /* get current vinfo */
   if (fbdpy->assume_fixed_vinfo) {
      vinfo = fbdpy->config_vinfo;
   }
   else {
      memset(&vinfo, 0, sizeof(vinfo));
      if (ioctl(fbdpy->fd, FBIOGET_VSCREENINFO, &vinfo)) {
         FREE(fbsurf);
         return NULL;
      }
   }

   fbsurf->width = vinfo.xres;
   fbsurf->height = vinfo.yres;

   if (!fbdev_surface_update_drawable(&fbsurf->base, &vinfo)) {
      FREE(fbsurf);
      return NULL;
   }

   fbsurf->rsurf = resource_surface_create(fbdpy->base.screen,
         nconf->color_format,
         PIPE_BIND_RENDER_TARGET |
         PIPE_BIND_DISPLAY_TARGET);
   if (!fbsurf->rsurf) {
      FREE(fbsurf);
      return NULL;
   }

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

   fbsurf->base.destroy = fbdev_surface_destroy;
   fbsurf->base.present = fbdev_surface_present;
   fbsurf->base.validate = fbdev_surface_validate;
   fbsurf->base.wait = fbdev_surface_wait;

   return &fbsurf->base;
}

static struct native_surface *
fbdev_display_create_scanout_surface(struct native_display *ndpy,
                                     const struct native_config *nconf,
                                     uint width, uint height)
{
   return fbdev_display_create_window_surface(ndpy,
         (EGLNativeWindowType) NULL, nconf);
}

static boolean
fbdev_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)
{
   return TRUE;
}

static const struct native_mode **
fbdev_display_get_modes(struct native_display *ndpy,
                        const struct native_connector *nconn,
                        int *num_modes)
{
   static struct native_mode mode;
   const struct native_mode **modes;

   if (!mode.desc) {
      struct fbdev_display *fbdpy = fbdev_display(ndpy);
      mode.desc = "Current Mode";
      mode.width = fbdpy->config_vinfo.xres;
      mode.height = fbdpy->config_vinfo.yres;
      mode.refresh_rate = 60 * 1000; /* dummy */
   }

   modes = MALLOC(sizeof(*modes));
   if (modes) {
      modes[0] = &mode;
      if (num_modes)
         *num_modes = 1;
   }

   return modes;
}

static const struct native_connector **
fbdev_display_get_connectors(struct native_display *ndpy, int *num_connectors,
                           int *num_crtc)
{
   static struct native_connector connector;
   const struct native_connector **connectors;

   connectors = MALLOC(sizeof(*connectors));
   if (connectors) {
      connectors[0] = &connector;
      if (num_connectors)
         *num_connectors = 1;
   }

   return connectors;
}

/* remove modeset support one day! */
static const struct native_display_modeset fbdev_display_modeset = {
   .get_connectors = fbdev_display_get_connectors,
   .get_modes = fbdev_display_get_modes,
   .create_scanout_surface = fbdev_display_create_scanout_surface,
   .program = fbdev_display_program
};

static const struct native_config **
fbdev_display_get_configs(struct native_display *ndpy, int *num_configs)
{
   struct fbdev_display *fbdpy = fbdev_display(ndpy);
   const struct native_config **configs;

   configs = MALLOC(sizeof(*configs));
   if (configs) {
      configs[0] = &fbdpy->config;
      if (num_configs)
         *num_configs = 1;
   }

   return configs;
}

static int
fbdev_display_get_param(struct native_display *ndpy,
                      enum native_param_type param)
{
   int val;

   switch (param) {
   case NATIVE_PARAM_PRESERVE_BUFFER:
      val = 1;
      break;
   case NATIVE_PARAM_USE_NATIVE_BUFFER:
   case NATIVE_PARAM_MAX_SWAP_INTERVAL:
   default:
      val = 0;
      break;
   }

   return val;
}

static void
fbdev_display_destroy(struct native_display *ndpy)
{
   struct fbdev_display *fbdpy = fbdev_display(ndpy);

   ndpy_uninit(&fbdpy->base);
   close(fbdpy->fd);
   FREE(fbdpy);
}

static boolean
fbdev_display_init_screen(struct native_display *ndpy)
{
   struct fbdev_display *fbdpy = fbdev_display(ndpy);
   struct sw_winsys *ws;

   ws = fbdev_create_sw_winsys(fbdpy->fd);
   if (!ws)
      return FALSE;

   fbdpy->base.screen = fbdpy->event_handler->new_sw_screen(&fbdpy->base, ws);
   if (!fbdpy->base.screen) {
      if (ws->destroy)
         ws->destroy(ws);
      return FALSE;
   }

   if (!fbdpy->base.screen->is_format_supported(fbdpy->base.screen,
            fbdpy->config.color_format, PIPE_TEXTURE_2D, 0,
            PIPE_BIND_RENDER_TARGET)) {
      fbdpy->base.screen->destroy(fbdpy->base.screen);
      fbdpy->base.screen = NULL;
      return FALSE;
   }

   return TRUE;
}

static boolean
fbdev_display_init_config(struct native_display *ndpy)
{
   struct fbdev_display *fbdpy = fbdev_display(ndpy);
   struct native_config *nconf = &fbdpy->config;

   if (ioctl(fbdpy->fd, FBIOGET_VSCREENINFO, &fbdpy->config_vinfo))
      return FALSE;

   nconf->color_format = vinfo_to_format(&fbdpy->config_vinfo);
   if (nconf->color_format == PIPE_FORMAT_NONE)
      return FALSE;

   nconf->buffer_mask = (1 << NATIVE_ATTACHMENT_BACK_LEFT);

   nconf->window_bit = TRUE;

   return TRUE;
}

static struct native_display *
fbdev_display_create(int fd, const struct native_event_handler *event_handler)
{
   struct fbdev_display *fbdpy;

   fbdpy = CALLOC_STRUCT(fbdev_display);
   if (!fbdpy)
      return NULL;

   fbdpy->fd = fd;
   fbdpy->event_handler = event_handler;

   if (ioctl(fbdpy->fd, FBIOGET_FSCREENINFO, &fbdpy->finfo))
      goto fail;

   if (fbdpy->finfo.visual != FB_VISUAL_TRUECOLOR ||
       fbdpy->finfo.type != FB_TYPE_PACKED_PIXELS)
      goto fail;

   if (!fbdev_display_init_config(&fbdpy->base))
      goto fail;

   fbdpy->assume_fixed_vinfo = TRUE;

   fbdpy->base.init_screen = fbdev_display_init_screen;
   fbdpy->base.destroy = fbdev_display_destroy;
   fbdpy->base.get_param = fbdev_display_get_param;
   fbdpy->base.get_configs = fbdev_display_get_configs;

   fbdpy->base.create_window_surface = fbdev_display_create_window_surface;

   /* we'd like to remove modeset support one day */
   fbdpy->config.scanout_bit = TRUE;
   fbdpy->base.modeset = &fbdev_display_modeset;

   return &fbdpy->base;

fail:
   FREE(fbdpy);
   return NULL;
}

static const struct native_event_handler *fbdev_event_handler;

static struct native_display *
native_create_display(void *dpy, boolean use_sw)
{
   struct native_display *ndpy;
   int fd;

   /* well, this makes fd 0 being ignored */
   if (!dpy) {
      fd = open("/dev/fb0", O_RDWR);
   }
   else {
      fd = dup((int) pointer_to_intptr(dpy));
   }
   if (fd < 0)
      return NULL;

   ndpy = fbdev_display_create(fd, fbdev_event_handler);
   if (!ndpy)
      close(fd);

   return ndpy;
}

static const struct native_platform fbdev_platform = {
   "FBDEV", /* name */
   native_create_display
};

const struct native_platform *
native_get_fbdev_platform(const struct native_event_handler *event_handler)
{
   fbdev_event_handler = event_handler;
   return &fbdev_platform;
}
