/*
 * Copyright 2024 Google LLC
 *
 * 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 "pebble_shapes.h"

#include "applib/app.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/gpath.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/gtransform.h"
#include "applib/graphics/text.h"
#include "util/trig.h"
#include "applib/ui/action_bar_layer.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/window.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "system/logging.h"

typedef enum {
  POINT,
  LINE,
  SQUARE,
  RECTANGLE,
  RECTANGLE_ROUND,
  CIRCLE,
  GPATH_TRIANGLE,
  GPATH_OPEN_BUCKET,
  MAX_SHAPES
} DrawShape;

#define NUM_SHAPES MAX_SHAPES
static GColor s_display_colors[NUM_SHAPES];

#define MAX_STROKE_WIDTH 20

static const GPathInfo s_triangle_path = {
  3,
  (GPoint[]) {
    { -10, 0 },
    { 0, 10 },
    { 10, 0 }
  }
};

static const GPathInfo s_bucket_path = {
  4,
  (GPoint[]) {
    { -10, 0 },
    { -10, 30 },
    { 10, 30 },
    { 10, 0 }
  }
};

static const int TARGET_FPS = 40;
static const int PIXEL_SPEED_PER_FRAME = 2;

#define ANGLE_DEGREES_TO_TRIG_ANGLE(angle) (((angle % 360) * TRIG_MAX_ANGLE) / 360)
#define MAX_SCALE 10

typedef enum {
  APP_STATE_FILL_NON_AA,
  APP_STATE_FILL_AA,
  APP_STATE_DRAW_NON_AA_NO_SW,
  APP_STATE_DRAW_AA_NO_SW,
  APP_STATE_DRAW_NON_AA_SW,
  APP_STATE_DRAW_AA_SW,

  // Add more above
  APP_STATE_NUM_STATES
} AppStateIndex;

typedef struct AppData {
  Window window;

  // Point properties
  GPoint point_p0;
  int16_t point_velocity_x;
  int16_t point_velocity_y;

  // Line properties
  GPoint line_p0;
  GPoint line_p1;
  int16_t line_velocity_x;
  int16_t line_velocity_y;

  // Square properties
  GRect square;
  int16_t square_velocity_x;
  int16_t square_velocity_y;

  // Rectangle properties
  GRect rect;
  int16_t rect_velocity_x;
  int16_t rect_velocity_y;

  // Rectangle Round properties
  GRect rectr;
  uint16_t rectr_radius;
  uint16_t rectr_corners_index;
  int16_t rectr_velocity_x;
  int16_t rectr_velocity_y;

  // Circle properties
  GPoint circle_origin;
  uint16_t circle_radius;
  int16_t circle_velocity_x;
  int16_t circle_velocity_y;
  GColor circle_color;

  // Triangle
  GPath *triangle;
  GPoint triangle_offset;
  int16_t triangle_velocity_x;
  int16_t triangle_velocity_y;

  // Bucket
  GPath *bucket;
  GPoint bucket_offset;
  int16_t bucket_velocity_x;
  int16_t bucket_velocity_y;

  // Generic properties that can be changed as property_index changes
  bool fill;
  int16_t color_index;

  int64_t time_started;
  uint32_t rendered_frames;

  bool moving;
  AppStateIndex state_index;
  int16_t stroke_width;
  bool antialiased;
} AppData;


static void log_state(AppData *data) {
  switch (data->state_index) {
    case APP_STATE_FILL_NON_AA:
      PBL_LOG(LOG_LEVEL_DEBUG, "State: Fill Non-Antialiased; SW: N/A (but currently: %d)",
              data->stroke_width);
      break;
    case APP_STATE_FILL_AA:
      PBL_LOG(LOG_LEVEL_DEBUG, "State: Fill Antialiased; SW: N/A (but currently: %d)",
              data->stroke_width);
      break;
    case APP_STATE_DRAW_NON_AA_NO_SW:
      PBL_LOG(LOG_LEVEL_DEBUG, "State: Draw Non-Antialiased; SW: N/A (but currently: %d)",
              data->stroke_width);
      break;
    case APP_STATE_DRAW_AA_NO_SW:
      PBL_LOG(LOG_LEVEL_DEBUG, "State: Draw Antialiased; SW: N/A (but currently: %d)",
              data->stroke_width);
      break;
    case APP_STATE_DRAW_NON_AA_SW:
      PBL_LOG(LOG_LEVEL_DEBUG, "State: Draw Non-Antialiased; SW: %d", data->stroke_width);
      break;
    case APP_STATE_DRAW_AA_SW:
      PBL_LOG(LOG_LEVEL_DEBUG, "State: Draw Antialiased; SW: %d", data->stroke_width);
      break;
    default:
      PBL_LOG(LOG_LEVEL_DEBUG, "Unknown State");
      break;
  }
}

static bool stroke_width_enabled(AppStateIndex state_index) {
  return ((state_index == APP_STATE_DRAW_AA_SW) || (state_index == APP_STATE_DRAW_NON_AA_SW));
}

static void update_state(AppData *data, AppStateIndex state_index) {
  data->state_index = state_index;
  data->fill = ((data->state_index == APP_STATE_FILL_NON_AA) ||
                (data->state_index == APP_STATE_FILL_AA));
  data->antialiased = ((data->state_index == APP_STATE_DRAW_AA_NO_SW) ||
                       (data->state_index == APP_STATE_DRAW_AA_SW) ||
                       (data->state_index == APP_STATE_FILL_AA));
}

static void back_handler(ClickRecognizerRef recognizer, void *context) {
  AppData *data = app_state_get_user_data();

  update_state(data, ((data->state_index - 1) % APP_STATE_NUM_STATES));
  log_state(data);
}

static void up_handler(ClickRecognizerRef recognizer, void *context) {
  AppData *data = app_state_get_user_data();

  if (stroke_width_enabled(data->state_index)) {
    data->stroke_width++;
    if (data->stroke_width >= MAX_STROKE_WIDTH) {
      data->stroke_width = 1;
    }
  }
  log_state(data);
}

static void select_handler(ClickRecognizerRef recognizer, void *context) {
  AppData *data = app_state_get_user_data();

  update_state(data, ((data->state_index + 1) % APP_STATE_NUM_STATES));
  log_state(data);
}

static void down_handler(ClickRecognizerRef recognizer, void *context) {
  AppData *data = app_state_get_user_data();

  data->moving = !data->moving;

  log_state(data);
}

static void click_config_provider(void *context) {
  window_single_click_subscribe(BUTTON_ID_BACK, back_handler);
  window_single_click_subscribe(BUTTON_ID_UP, up_handler);
  window_single_click_subscribe(BUTTON_ID_SELECT, select_handler);
  window_single_click_subscribe(BUTTON_ID_DOWN, down_handler);
}

static void prv_move_shape(AppData *data) {
  for (uint8_t shape = POINT; shape < MAX_SHAPES; shape++) {
    if (shape == POINT) {
      // Move the point 4*X per Y
      data->point_p0.x += (data->point_velocity_x * PIXEL_SPEED_PER_FRAME * 4);
      if (data->point_p0.x < 0 || data->point_p0.x > data->window.layer.bounds.size.w) {
        data->point_velocity_x = data->point_velocity_x * -1;
      }

      data->point_p0.y += (data->point_velocity_y * PIXEL_SPEED_PER_FRAME);
      if (data->point_p0.y < 0 || data->point_p0.y > data->window.layer.bounds.size.h) {
        data->point_velocity_y = data->point_velocity_y * -1;
      }
    } else if (shape == LINE) {
      // Move the line 2*X per Y
      data->line_p0.x += (data->line_velocity_x * PIXEL_SPEED_PER_FRAME * 2);
      data->line_p1.x += (data->line_velocity_x * PIXEL_SPEED_PER_FRAME * 2);
      if (data->line_p0.x < 0 || data->line_p0.x > data->window.layer.bounds.size.w ||
          data->line_p1.x < 0 || data->line_p1.x > data->window.layer.bounds.size.w ) {
        data->line_velocity_x = data->line_velocity_x * -1;
      }

      data->line_p0.y += (data->line_velocity_y * PIXEL_SPEED_PER_FRAME);
      data->line_p1.y += (data->line_velocity_y * PIXEL_SPEED_PER_FRAME);
      if (data->line_p0.y < 0 || data->line_p0.y > data->window.layer.bounds.size.h ||
          data->line_p1.y < 0 || data->line_p1.y > data->window.layer.bounds.size.h ) {
        data->line_velocity_y = data->line_velocity_y * -1;
      }
    } else if (shape == SQUARE) {
      // Move the square X per Y
      data->square.origin.x += (data->square_velocity_x * PIXEL_SPEED_PER_FRAME);
      if (data->square.origin.x < 0 ||
          data->square.origin.x + data->square.size.w > data->window.layer.bounds.size.w) {
        data->square_velocity_x = data->square_velocity_x * -1;
      }

      data->square.origin.y += (data->square_velocity_y * PIXEL_SPEED_PER_FRAME);
      if (data->square.origin.y < 0 ||
          data->square.origin.y + data->square.size.h > data->window.layer.bounds.size.h) {
        data->square_velocity_y = data->square_velocity_y * -1;
      }
    } else if (shape == RECTANGLE) {
      // Move the rectangle X per 2*Y
      data->rect.origin.x += (data->rect_velocity_x * PIXEL_SPEED_PER_FRAME);
      if (data->rect.origin.x < 0 ||
          data->rect.origin.x + data->rect.size.w > data->window.layer.bounds.size.w) {
        data->rect_velocity_x = data->rect_velocity_x * -1;
      }

      data->rect.origin.y += (data->rect_velocity_y * PIXEL_SPEED_PER_FRAME * 2);
      if (data->rect.origin.y < 0 ||
          data->rect.origin.y + data->rect.size.h > data->window.layer.bounds.size.h) {
        data->rect_velocity_y = data->rect_velocity_y * -1;
      }
    } else if (shape == RECTANGLE_ROUND) {
      // Move rounded line X per 4*Y
      data->rectr.origin.x += (data->rectr_velocity_x * PIXEL_SPEED_PER_FRAME);
      if (data->rectr.origin.x < 0 ||
          data->rectr.origin.x + data->rectr.size.w > data->window.layer.bounds.size.w) {
        data->rectr_velocity_x = data->rectr_velocity_x * -1;
      }

      data->rectr.origin.y += (data->rectr_velocity_y * PIXEL_SPEED_PER_FRAME * 4);
      if (data->rectr.origin.y < 0 ||
          data->rectr.origin.y + data->rectr.size.h > data->window.layer.bounds.size.h) {
        data->rectr_velocity_y = data->rectr_velocity_y * -1;
      }
    } else if (shape == CIRCLE) {
      // Move the line X per Y
      data->circle_origin.x += (data->circle_velocity_x * PIXEL_SPEED_PER_FRAME);
      if (data->circle_origin.x - data->circle_radius < 0 ||
          data->circle_origin.x + data->circle_radius  > data->window.layer.bounds.size.w) {
        data->circle_velocity_x = data->circle_velocity_x * -1;
        data->circle_color.argb = (((data->circle_color.argb + 1) & 0x3F) | 0xC0);
      }

      data->circle_origin.y += (data->circle_velocity_y * PIXEL_SPEED_PER_FRAME);
      if (data->circle_origin.y - data->circle_radius < 0 ||
          data->circle_origin.y + data->circle_radius  > data->window.layer.bounds.size.h) {
        data->circle_velocity_y = data->circle_velocity_y * -1;
        data->circle_color.argb = (((data->circle_color.argb + 1) & 0x3F) | 0xC0);
      }
    } else if (shape == GPATH_TRIANGLE) {
      // Move the line 3*X per Y
      data->triangle_offset.x += (data->triangle_velocity_x * PIXEL_SPEED_PER_FRAME * 3);
      if (data->triangle_offset.x < 0 ||
          data->triangle_offset.x > data->window.layer.bounds.size.w) {
        data->triangle_velocity_x = data->triangle_velocity_x * -1;
      }

      data->triangle_offset.y += (data->triangle_velocity_y * PIXEL_SPEED_PER_FRAME);
      if (data->triangle_offset.y < 0 ||
          data->triangle_offset.y > data->window.layer.bounds.size.h) {
        data->triangle_velocity_y = data->triangle_velocity_y * -1;
      }
      gpath_move_to(data->triangle, data->triangle_offset);
    } else if (shape == GPATH_OPEN_BUCKET) {
      // Move the line 2*X per 3*Y
      data->bucket_offset.x += (data->bucket_velocity_x * PIXEL_SPEED_PER_FRAME * 2);
      if (data->bucket_offset.x < 0 ||
          data->bucket_offset.x > data->window.layer.bounds.size.w) {
        data->bucket_velocity_x = data->bucket_velocity_x * -1;
      }

      data->bucket_offset.y += (data->bucket_velocity_y * PIXEL_SPEED_PER_FRAME * 3);
      if (data->bucket_offset.y < 0 ||
          data->bucket_offset.y > data->window.layer.bounds.size.h) {
        data->bucket_velocity_y = data->bucket_velocity_y * -1;
      }
      gpath_move_to(data->bucket, data->bucket_offset);
    }
  }
}

static void draw_shape(GContext* ctx, AppData *data, DrawShape shape, GColor color) {
  graphics_context_set_fill_color(ctx, color);
  graphics_context_set_stroke_color(ctx, color);
  if (data->fill) {
    if (shape == POINT) {
      graphics_draw_pixel(ctx, data->point_p0);
    } else if (shape == LINE) {
      graphics_draw_line(ctx, data->line_p0, data->line_p1);
    } else if (shape == SQUARE) {
      graphics_fill_rect(ctx, &data->square);
    } else if (shape == RECTANGLE) {
      graphics_fill_rect(ctx, &data->rect);
    } else if (shape == RECTANGLE_ROUND) {
      graphics_fill_round_rect(ctx, &data->rectr, data->rectr_radius, GCornersAll);
    } else if (shape == CIRCLE) {
      graphics_context_set_fill_color(ctx, data->circle_color);
      graphics_context_set_stroke_color(ctx, data->circle_color);
      graphics_fill_circle(ctx, data->circle_origin, data->circle_radius);
    } else if (shape == GPATH_TRIANGLE) {
      gpath_draw_filled(ctx, data->triangle);
    } else if (shape == GPATH_OPEN_BUCKET) {
      gpath_draw_filled(ctx, data->bucket);
    }
  } else {
    if (shape == POINT) {
      graphics_draw_pixel(ctx, data->point_p0);
    } else if (shape == LINE) {
      graphics_draw_line(ctx, data->line_p0, data->line_p1);
    } else if (shape == SQUARE) {
      graphics_draw_rect(ctx, &data->square);
    } else if (shape == RECTANGLE) {
      graphics_draw_rect(ctx, &data->rect);
    } else if (shape == RECTANGLE_ROUND) {
      graphics_draw_round_rect(ctx, &data->rectr, data->rectr_radius);
    } else if (shape == CIRCLE) {
      graphics_context_set_fill_color(ctx, data->circle_color);
      graphics_context_set_stroke_color(ctx, data->circle_color);
      graphics_draw_circle(ctx, data->circle_origin, data->circle_radius);
    } else if (shape == GPATH_TRIANGLE) {
      gpath_draw_outline(ctx, data->triangle);
    } else if (shape == GPATH_OPEN_BUCKET) {
      gpath_draw_outline_open(ctx, data->bucket);
    }
  }
}

static int64_t prv_time_64(void) {
  time_t s;
  uint16_t ms;
  rtc_get_time_ms(&s, &ms);
  return (int64_t)s * 1000 + ms;
}

static void layer_update_proc(Layer *layer, GContext* ctx) {
  AppData *data = app_state_get_user_data();

  graphics_context_set_fill_color(ctx, GColorBlack);
  if (stroke_width_enabled(data->state_index)) {
    graphics_context_set_stroke_width(ctx, data->stroke_width);
  } else {
    graphics_context_set_stroke_width(ctx, 1);
  }
  graphics_context_set_antialiased(ctx, data->antialiased);

  graphics_fill_rect(ctx, &layer->bounds);

  uint8_t color_index = 0;
  for (uint8_t shape_index = POINT; shape_index < MAX_SHAPES; shape_index++) {
    draw_shape(ctx, data, (DrawShape)shape_index, (GColor)s_display_colors[color_index++]);
  }


  if (data->rendered_frames == 0) {
    data->time_started = prv_time_64();
  } else {
    int64_t time_rendered = prv_time_64() - data->time_started;
    if ((data->rendered_frames % 64) == 0) {
      PBL_LOG(LOG_LEVEL_DEBUG, "## %d frames rendered", (int)data->rendered_frames);
      PBL_LOG(LOG_LEVEL_DEBUG, "## at %"PRIu32" FPS",
              (uint32_t)((uint64_t)data->rendered_frames*1000/time_rendered));
    }
  }
  data->rendered_frames++;
}


static void timer_callback(void *cb_data) {
  AppData *data = app_state_get_user_data();

  if (data->moving) {
    prv_move_shape(data);
  }

  layer_mark_dirty(&data->window.layer);

  app_timer_register(1000 / TARGET_FPS, timer_callback, NULL);
}

static void init(void) {
  AppData *data = task_malloc_check(sizeof(AppData));
  memset(data, 0x00, sizeof(AppData));

  s_display_colors[0] = GColorWhite;
  s_display_colors[1] = GColorRed;
  s_display_colors[2] = GColorGreen;
  s_display_colors[3] = GColorBlue;
  s_display_colors[4] = (GColor)((uint8_t)0b11111100);
  s_display_colors[5] = (GColor)((uint8_t)0b11001111);
  s_display_colors[6] = (GColor)((uint8_t)0b11110101);
  s_display_colors[7] = GColorWhite;


  app_state_set_user_data(data);

  Window *window = &data->window;
  window_init(window, WINDOW_NAME("Shapes"));
  window_set_user_data(window, data);
  window_set_fullscreen(window, true);
  layer_set_update_proc(&window->layer, layer_update_proc);

  window_set_click_config_provider(window, click_config_provider);

  const bool animated = true;
  app_window_stack_push(window, animated);

  // Initialize shapes

  // Point properties
  data->point_p0 = GPoint(1, 1);
  data->point_velocity_x = 1;
  data->point_velocity_y = 1;

  // Line properties
  data->line_p0 = GPoint(0, 0);
  data->line_p1 = GPoint(10, 10);
  data->line_velocity_x = 1;
  data->line_velocity_y = 1;

  // Square properties
  data->square = GRect(100, 50, 20, 20);
  data->square_velocity_x = 1;
  data->square_velocity_y = 1;

  // Rectangle properties
  data->rect = GRect(80, 0, 30, 50);
  data->rect_velocity_x = 1;
  data->rect_velocity_y = 1;

  // Rectangle Round properties
  data->rectr = GRect(20, 20, 20, 30);
  data->rectr_radius = 5;
  data->rectr_corners_index = GCornersAll;
  data->rectr_velocity_x = 1;
  data->rectr_velocity_y = 1;

  // Circle properties
  data->circle_origin = GPoint(50, 50);
  data->circle_radius = 20;
  data->circle_velocity_x = 1;
  data->circle_velocity_y = 1;
  data->circle_color = s_display_colors[5];

  // Triangle
  data->triangle = gpath_create(&s_triangle_path);
  data->triangle_offset = GPoint(10, 80);
  gpath_move_to(data->triangle, data->triangle_offset);
  data->triangle_velocity_x = 1;
  data->triangle_velocity_y = 1;

  // Open bucket
  data->bucket = gpath_create(&s_bucket_path);
  data->bucket_offset = GPoint(20, 30);
  gpath_move_to(data->bucket, data->bucket_offset);
  data->bucket_velocity_x = 1;
  data->bucket_velocity_y = 1;

  // Other properties
  update_state(data, APP_STATE_FILL_NON_AA);
  data->stroke_width = 1;
  data->moving = true;
  app_timer_register(33, timer_callback, NULL);
}

static void deinit(void) {
  AppData *data = app_state_get_user_data();
  task_free(data);
}

static void s_main(void) {
  init();

  app_event_loop();

  deinit();
}

const PebbleProcessMd* pebble_shapes_get_app_info(void) {
  static const PebbleProcessMdSystem s_app_info = {
    .common.main_func = s_main,
    .name = "Pebble Shapes"
  };
  return (const PebbleProcessMd*) &s_app_info;
}
