/* Copyright 2017-present Renesas Electronics Corporation and other contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "iotjs_def.h"

#if defined(USE_POSIX_BRIDGE)
#include "peripheral_io.h"
#include "iotjs_module_aligned_buffer.h"
#include "iotjs_module_display.h"


IOTJS_DEFINE_NATIVE_HANDLE_INFO_THIS_MODULE(display);

struct iotjs_display_platform_data_s {
  lcd_h handle;
};

jerry_value_t iotjs_display_set_platform_config(iotjs_display_t* display,
                                             const jerry_value_t jconfig) {
  return jerry_create_undefined();
}

void iotjs_display_create_platform_data(iotjs_display_t* display) {
  display->platform_data = IOTJS_ALLOC(iotjs_display_platform_data_t);
}

void iotjs_display_destroy_platform_data(
    iotjs_display_platform_data_t* platform_data) {
  IOTJS_RELEASE(platform_data);
}


IOTJS_DEFINE_PERIPH_CREATE_FUNCTION(display);

static uint32_t get_pixel_bytes(display_pixel_format_t format) {
	switch(format) {
	case DISPLAY_PIXELFORMAT_YCBCR422:
	case DISPLAY_PIXELFORMAT_RGB565:
	case DISPLAY_PIXELFORMAT_ARGB4444:
		return 2;
	case DISPLAY_PIXELFORMAT_RGB888:
	case DISPLAY_PIXELFORMAT_ARGB8888:
		return 4;
	default:
		return 0;
	}
}

static int push_lcd_layer(iotjs_display_t* display, lcd_layer_t layer) {
  lcd_layer_t* lcd_layer_ptr = display->lcd_layer;
  int i;
  for(i = 0; i < LCDLAYER_MAX; i++) {
    if(lcd_layer_ptr->id >= LCDLAYER_MAX) {
      *lcd_layer_ptr = layer;
      break;
    }
    lcd_layer_ptr++;
  }
  return (i < LCDLAYER_MAX) ? 0 : -1;
}

static lcd_layer_t pop_lcd_layer(iotjs_display_t* display) {
  lcd_layer_t ret_lcd_layer = display->lcd_layer[0];
  lcd_layer_t* lcd_layer_ptr = display->lcd_layer;
  int i;
  for(i = 0; i < LCDLAYER_MAX - 1; i++) {
    *lcd_layer_ptr = *(lcd_layer_ptr+1);
    lcd_layer_ptr++;
  }
  lcd_layer_ptr->id = LCDLAYER_MAX;
  lcd_layer_ptr->frame_buffer = NULL;

  return ret_lcd_layer;
}

static int push_lcd_layer_id(iotjs_display_t* display, lcd_layer_id_t id) {
  lcd_layer_id_t* layer_id_ptr = display->layer_id;
  int i;
  for(i = 0; i < LCDLAYER_MAX; i++) {
    if(*layer_id_ptr >= LCDLAYER_MAX) {
      *layer_id_ptr = id;
      break;
    }
    layer_id_ptr++;
  }
  return (i < LCDLAYER_MAX) ? 0 : -1;
}

static lcd_layer_id_t pop_lcd_layer_id(iotjs_display_t* display) {
  lcd_layer_id_t ret_layer_id = display->layer_id[0];
  lcd_layer_id_t* layer_id_ptr = display->layer_id;
  int i;
  for(i = 0; i < LCDLAYER_MAX - 1; i++) {
    *layer_id_ptr = *(layer_id_ptr+1);
    layer_id_ptr++;
  }
  *layer_id_ptr = LCDLAYER_MAX;

  return ret_layer_id;
}

bool iotjs_display_open(iotjs_display_t* display) {
  lcd_t* lcd = &display->lcd;

  lcd_h handle = lcd_open(lcd);
  if( !handle ) {
    return false;
  }
  display->platform_data->handle = handle;

  jerry_value_t jdisplay = display->jobject;
  iotjs_jval_set_property_number(jdisplay, "width", lcd->width);
  iotjs_jval_set_property_number(jdisplay, "height", lcd->height);

  return true;
}

bool iotjs_display_start(iotjs_display_t* display) {
  return lcd_start(display->platform_data->handle, pop_lcd_layer(display));
}

bool iotjs_display_stop(iotjs_display_t* display) {
  return lcd_stop(display->platform_data->handle, pop_lcd_layer_id(display));
}

bool iotjs_display_update(iotjs_display_t* display) {
  return lcd_update(display->platform_data->handle, pop_lcd_layer(display));
}

bool iotjs_display_close(iotjs_display_t* display) {
  bool ret = lcd_close(display->platform_data->handle);
  display->platform_data->handle = 0;
  return ret;
}

static void iotjs_display_destroy(iotjs_display_t* display) {
  iotjs_display_destroy_platform_data(display->platform_data);
  IOTJS_RELEASE(display);
}

static void display_worker(uv_work_t* work_req) {
  iotjs_periph_reqwrap_t* req_wrap =
      (iotjs_periph_reqwrap_t*)(iotjs_reqwrap_from_request(
          (uv_req_t*)work_req));
  iotjs_display_t* display = (iotjs_display_t*)req_wrap->data;

  switch (req_wrap->op) {
    case kDisplayOpOpen:
      req_wrap->result = iotjs_display_open(display);
      break;
    case kDisplayOpStart:
      req_wrap->result = iotjs_display_start(display);
      break;
    case kDisplayOpStop:
      req_wrap->result = iotjs_display_stop(display);
      break;
    case kDisplayOpUpdate:
      req_wrap->result = iotjs_display_update(display);
      break;
    case kDisplayOpClose:
      req_wrap->result = iotjs_display_close(display);
      break;
    default:
      IOTJS_ASSERT(!"Invalid Operation");
  }
}

static jerry_value_t display_set_configuration(iotjs_display_t* display,
                                            jerry_value_t jconfig) {
  jerry_value_t jtype = iotjs_jval_get_property(jconfig, "type");
  if (jerry_value_is_number(jtype)) {
    display->lcd.type = (lcd_type_t)iotjs_jval_as_number(jtype);
  } else {
    display->lcd.type = LCDTYPE_MAX;
  }
  jerry_release_value(jtype);
  if(display->lcd.type >= LCDTYPE_MAX) {
    return JS_CREATE_ERROR(TYPE, "Invalid config.type");
  }

  return jerry_create_undefined();
}

static jerry_value_t display_get_start_param(iotjs_display_t* display,
                                            const jerry_value_t jargv[],
                                            const jerry_length_t jargc) {
  if(jargc < 4) {
    return JS_CREATE_ERROR(TYPE, "Invalid Arguments");
  }

  lcd_layer_t lcd_layer;

  lcd_layer_id_t layer_id = (lcd_layer_id_t)JS_GET_ARG(1, number);
  if(layer_id >= LCDLAYER_MAX) {
    return JS_CREATE_ERROR(TYPE, "Invalid Layer ID");
  }
  lcd_layer.id = layer_id;

  const jerry_value_t jbuffer = JS_GET_ARG(2, object);
  iotjs_aligned_buffer_wrap_t* buffer_wrap = iotjs_aligned_buffer_wrap_from_jbuffer(jbuffer);
  lcd_layer.frame_buffer = iotjs_aligned_buffer_wrap_native_buffer_ptr(buffer_wrap);
  if(((buffer_wrap->alignment % 32) != 0) || (((int)lcd_layer.frame_buffer % 32) != 0)) {
    return JS_CREATE_ERROR(TYPE, "Invalid buffer alignment");
  }

  display_pixel_format_t format = (display_pixel_format_t)JS_GET_ARG(3, number);
  if(format >= DISPLAY_PIXELFORMAT_MAX) {
    return JS_CREATE_ERROR(TYPE, "Invalid Buffer Format");
  }
  lcd_layer.buffer_format = format;
  lcd_layer.pixel_bytes = get_pixel_bytes(format);

  int ret = push_lcd_layer(display, lcd_layer);
  if(ret < 0) {
    return JS_CREATE_ERROR(COMMON, "Event Queue Full");
  }

  return jerry_create_undefined();
}


static jerry_value_t display_get_stop_param(iotjs_display_t* display,
                                            const jerry_value_t jargv[],
                                            const jerry_length_t jargc) {
  if(jargc < 1) {
    return JS_CREATE_ERROR(TYPE, "Invalid Arguments");
  }

  lcd_layer_id_t layer_id = (lcd_layer_id_t)JS_GET_ARG(0, number);
  if(layer_id >= LCDLAYER_MAX) {
    return JS_CREATE_ERROR(TYPE, "Invalid Layer ID");
  }
  int ret = push_lcd_layer_id(display, layer_id);
  if(ret < 0) {
    return JS_CREATE_ERROR(COMMON, "Event Queue Full");
  }

  return jerry_create_undefined();
}


static jerry_value_t display_get_update_param(iotjs_display_t* display,
                                            const jerry_value_t jargv[],
                                            const jerry_length_t jargc) {
  if(jargc < 2) {
    return JS_CREATE_ERROR(TYPE, "Invalid Arguments");
  }

  lcd_layer_t lcd_layer;

  lcd_layer_id_t layer_id = (lcd_layer_id_t)JS_GET_ARG(0, number);
  if(layer_id >= LCDLAYER_MAX) {
    return JS_CREATE_ERROR(TYPE, "Invalid Layer ID");
  }
  lcd_layer.id = layer_id;

  const jerry_value_t jbuffer = JS_GET_ARG(1, object);
  iotjs_aligned_buffer_wrap_t* buffer_wrap = iotjs_aligned_buffer_wrap_from_jbuffer(jbuffer);
  lcd_layer.frame_buffer = iotjs_aligned_buffer_wrap_native_buffer_ptr(buffer_wrap);
  if(((buffer_wrap->alignment % 32) != 0) || (((int)lcd_layer.frame_buffer % 32) != 0)) {
    return JS_CREATE_ERROR(TYPE, "Invalid buffer alignment");
  }

  int ret = push_lcd_layer(display, lcd_layer);
  if(ret < 0) {
    return JS_CREATE_ERROR(COMMON, "Event Queue Full");
  }

  return jerry_create_undefined();
}


JS_FUNCTION(DisplayCons) {
  DJS_CHECK_THIS();
  DJS_CHECK_ARGS(1, object);
  DJS_CHECK_ARG_IF_EXIST(1, function);

  // Create Display object
  jerry_value_t jdisplay = JS_GET_THIS();
  iotjs_display_t* display = display_create(jdisplay);

  jerry_value_t jconfig = JS_GET_ARG(0, object);

  // set configuration
  jerry_value_t res = iotjs_display_set_platform_config(display, jconfig);
  if (jerry_value_is_error(res)) {
    return res;
  }

  res = display_set_configuration(display, jconfig);
  if (jerry_value_is_error(res)) {
    return res;
  }

  for(int i = 0; i < LCDLAYER_MAX; i++) {
    display->lcd_layer[i].id = LCDLAYER_MAX;
    display->layer_id[i] = LCDLAYER_MAX;
  }

  jerry_value_t jcallback = JS_GET_ARG_IF_EXIST(1, function);

  // If the callback doesn't exist, it is completed synchronously.
  // Otherwise, it will be executed asynchronously.
  if (!jerry_value_is_null(jcallback)) {
    iotjs_ext_periph_call_async(display, jcallback, kDisplayOpOpen, display_worker);
  } else if (!iotjs_display_open(display)) {
    return JS_CREATE_ERROR(COMMON, iotjs_ext_periph_error_str(kDisplayOpOpen));
  }

  return jerry_create_undefined();
}


JS_FUNCTION(Start) {
  DJS_CHECK_ARGS(4, object, number, object, number);
  JS_DECLARE_OBJECT_PTR(0, display, display);

  jerry_value_t res = display_get_start_param(display, jargv, jargc);
  if (jerry_value_is_error(res)) {
    return res;
  }

  iotjs_ext_periph_call_async(display, JS_GET_ARG_IF_EXIST(4, function), kDisplayOpStart,
                          display_worker);
  return jerry_create_undefined();
}

JS_FUNCTION(StartSync) {
  DJS_CHECK_ARGS(4, object, number, object, number);
  JS_DECLARE_OBJECT_PTR(0, display, display);

  jerry_value_t res = display_get_start_param(display, jargv, jargc);
  if (jerry_value_is_error(res)) {
    return res;
  }

  if(!iotjs_display_start(display)) {
    return JS_CREATE_ERROR(COMMON, iotjs_ext_periph_error_str(kDisplayOpStart));
  }

  return jerry_create_undefined();
}

JS_FUNCTION(Stop) {
  JS_DECLARE_THIS_PTR(display, display);
  DJS_CHECK_ARGS(1, number);

  jerry_value_t res = display_get_stop_param(display, jargv, jargc);
  if (jerry_value_is_error(res)) {
    return res;
  }

  iotjs_ext_periph_call_async(display, JS_GET_ARG_IF_EXIST(1, function), kDisplayOpStop,
                          display_worker);
  return jerry_create_undefined();
}

JS_FUNCTION(StopSync) {
  JS_DECLARE_THIS_PTR(display, display);
  DJS_CHECK_ARGS(1, number);

  jerry_value_t res = display_get_stop_param(display, jargv, jargc);
  if (jerry_value_is_error(res)) {
    return res;
  }

  if(!iotjs_display_stop(display)) {
    return JS_CREATE_ERROR(COMMON, iotjs_ext_periph_error_str(kDisplayOpStop));
  }

  return jerry_create_undefined();
}

JS_FUNCTION(Update) {
  JS_DECLARE_THIS_PTR(display, display);
  DJS_CHECK_ARGS(2, number, object);

  jerry_value_t res = display_get_update_param(display, jargv, jargc);
  if (jerry_value_is_error(res)) {
    return res;
  }

  iotjs_ext_periph_call_async(display, JS_GET_ARG_IF_EXIST(2, function), kDisplayOpUpdate,
                          display_worker);
  return jerry_create_undefined();
}

JS_FUNCTION(UpdateSync) {
  JS_DECLARE_THIS_PTR(display, display);
  DJS_CHECK_ARGS(2, number, object);

  jerry_value_t res = display_get_update_param(display, jargv, jargc);
  if (jerry_value_is_error(res)) {
    return res;
  }

  if(!iotjs_display_update(display)) {
    return JS_CREATE_ERROR(COMMON, iotjs_ext_periph_error_str(kDisplayOpUpdate));
  }

  return jerry_create_undefined();
}

JS_FUNCTION(Close) {
  JS_DECLARE_OBJECT_PTR(0, display, display);
  iotjs_ext_periph_call_async(display, JS_GET_ARG_IF_EXIST(1, function), kDisplayOpClose,
                          display_worker);
  return jerry_create_undefined();
}

JS_FUNCTION(CloseSync) {
  JS_DECLARE_OBJECT_PTR(0, display, display);
  if(!iotjs_display_close(display)) {
    return JS_CREATE_ERROR(COMMON, iotjs_ext_periph_error_str(kDisplayOpClose));
  }
  return jerry_create_undefined();
}

#endif	// #if defined(USE_POSIX_BRIDGE)


jerry_value_t InitDisplay() {

#if defined(USE_POSIX_BRIDGE)
  jerry_value_t jdisplay_cons = jerry_create_external_function(DisplayCons);
  iotjs_jval_set_method(jdisplay_cons, "startImpl", Start);
  iotjs_jval_set_method(jdisplay_cons, "startSyncImpl", StartSync);
  iotjs_jval_set_method(jdisplay_cons, "closeImpl", Close);
  iotjs_jval_set_method(jdisplay_cons, "closeSyncImpl", CloseSync);

  jerry_value_t prototype = jerry_create_object();
  iotjs_jval_set_method(prototype, "stop", Stop);
  iotjs_jval_set_method(prototype, "stopSync", StopSync);
  iotjs_jval_set_method(prototype, "update", Update);
  iotjs_jval_set_method(prototype, "updateSync", UpdateSync);

  iotjs_jval_set_property_jval(jdisplay_cons, IOTJS_MAGIC_STRING_PROTOTYPE,
                               prototype);
  jerry_release_value(prototype);

  // LCD Type
  jerry_value_t jlcd = jerry_create_object();
  iotjs_jval_set_property_number(jlcd, "4_3INCH", LCDTYPE_4_3INCH);
  iotjs_jval_set_property_number(jlcd, "MAX", LCDTYPE_MAX);
  iotjs_jval_set_property_jval(jdisplay_cons, "LCDTYPE", jlcd);
  jerry_release_value(jlcd);

  // Buffer Format
  jerry_value_t jpixel = jerry_create_object();
  iotjs_jval_set_property_number(jpixel, "YCBCR422", DISPLAY_PIXELFORMAT_YCBCR422);
  iotjs_jval_set_property_number(jpixel, "RGB565", DISPLAY_PIXELFORMAT_RGB565);
  iotjs_jval_set_property_number(jpixel, "RGB888", DISPLAY_PIXELFORMAT_RGB888);
  iotjs_jval_set_property_number(jpixel, "ARGB8888", DISPLAY_PIXELFORMAT_ARGB8888);
  iotjs_jval_set_property_number(jpixel, "ARGB4444", DISPLAY_PIXELFORMAT_ARGB4444);
  iotjs_jval_set_property_number(jpixel, "MAX", DISPLAY_PIXELFORMAT_MAX);
  iotjs_jval_set_property_jval(jdisplay_cons, "BUFFERFORMAT", jpixel);
  jerry_release_value(jpixel);

#else	// #if defined(USE_POSIX_BRIDGE)
  jerry_value_t jdisplay_cons = jerry_create_object();
#endif	// #if defined(USE_POSIX_BRIDGE)

  return jdisplay_cons;
}
