/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2024 by The BRLTTY Developers.
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

#include "parameters.h"
#include "blink.h"
#include "prefs.h"
#include "async_handle.h"
#include "async_alarm.h"
#include "update.h"
#include "core.h"

struct BlinkDescriptorStruct {
  const char *const name;
  const unsigned char *const isEnabled;
  unsigned char *const visibleTime;
  unsigned char *const invisibleTime;

  unsigned isRequired:1;
  unsigned isVisible:1;
  AsyncHandle alarmHandle;
};

BlinkDescriptor screenCursorBlinkDescriptor = {
  .name = "Screen Cursor",
  .isEnabled = &prefs.blinkingScreenCursor,
  .visibleTime = &prefs.screenCursorVisibleTime,
  .invisibleTime = &prefs.screenCursorInvisibleTime
};

BlinkDescriptor attributesUnderlineBlinkDescriptor = {
  .name = "Attributes Underline",
  .isEnabled = &prefs.blinkingAttributes,
  .visibleTime = &prefs.attributesVisibleTime,
  .invisibleTime = &prefs.attributesInvisibleTime
};

BlinkDescriptor uppercaseLettersBlinkDescriptor = {
  .name = "Uppercase Letters",
  .isEnabled = &prefs.blinkingCapitals,
  .visibleTime = &prefs.capitalsVisibleTime,
  .invisibleTime = &prefs.capitalsInvisibleTime
};

BlinkDescriptor speechCursorBlinkDescriptor = {
  .name = "Speech Cursor",
  .isEnabled = &prefs.blinkingSpeechCursor,
  .visibleTime = &prefs.speechCursorVisibleTime,
  .invisibleTime = &prefs.speechCursorInvisibleTime
};

static BlinkDescriptor *const blinkDescriptors[] = {
  &screenCursorBlinkDescriptor,
  &attributesUnderlineBlinkDescriptor,
  &uppercaseLettersBlinkDescriptor,
  &speechCursorBlinkDescriptor,
  NULL
};

static inline int
toPercentage (int numerator, int denominator) {
  return (numerator * 100) / denominator;
}

const char *
getBlinkName (BlinkDescriptor *blink) {
  return blink->name;
}

int
getBlinkVisibleTime (BlinkDescriptor *blink) {
  return PREFS2MSECS(*blink->visibleTime);
}

int
getBlinkInvisibleTime (BlinkDescriptor *blink) {
  return PREFS2MSECS(*blink->invisibleTime);
}

int
isBlinkEnabled (BlinkDescriptor *blink) {
  return *blink->isEnabled;
}

int
getBlinkPeriod (BlinkDescriptor *blink) {
  return getBlinkVisibleTime(blink) + getBlinkInvisibleTime(blink);
}

int
getBlinkPercentVisible (BlinkDescriptor *blink) {
  return toPercentage(getBlinkVisibleTime(blink), getBlinkPeriod(blink));
}

int
setBlinkProperties (BlinkDescriptor *blink, int period, int percentVisible) {
  if (period < 1) return 0;

  if (percentVisible < 1) return 0;
  if (percentVisible > 99) return 0;

  const int minimumTime = SCREEN_UPDATE_SCHEDULE_DELAY;
  period = MAX(period, (minimumTime * 2));

  int visibleTime;
  int invisibleTime;

  // let the visible time round toward 50%
  if (percentVisible < 50) {
    invisibleTime = (period * (100 - percentVisible)) / 100;
    visibleTime = period - invisibleTime;
  } else {
    visibleTime = (period * percentVisible) / 100;
    invisibleTime = period - visibleTime;
  }

  if (visibleTime && (visibleTime < minimumTime)) {
    visibleTime = minimumTime;
    invisibleTime = period - visibleTime;
  } else if (invisibleTime && (invisibleTime < minimumTime)) {
    invisibleTime = minimumTime;
    visibleTime = period - invisibleTime;
  }

  visibleTime = MSECS2PREFS(visibleTime);
  invisibleTime = MSECS2PREFS(invisibleTime);
  if ((visibleTime + invisibleTime) > UINT8_MAX) return 0;

  *blink->visibleTime = visibleTime;
  *blink->invisibleTime = invisibleTime;
  return 1;
}

int
isBlinkVisible (const BlinkDescriptor *blink) {
  if (!*blink->isEnabled) return 1;
  return blink->isVisible;
}

static int
getBlinkDuration (const BlinkDescriptor *blink) {
  return PREFS2MSECS(blink->isVisible? *blink->visibleTime: *blink->invisibleTime);
}

void
setBlinkState (BlinkDescriptor *blink, int visible) {
  int changed = visible != blink->isVisible;

  blink->isVisible = visible;

  if (blink->alarmHandle) {
    asyncResetAlarmIn(blink->alarmHandle, getBlinkDuration(blink));
    if (changed) scheduleUpdate("blink state set");
  }
}

static void setBlinkAlarm (BlinkDescriptor *blink);

ASYNC_ALARM_CALLBACK(handleBlinkAlarm) {
  BlinkDescriptor *blink = parameters->data;

  asyncDiscardHandle(blink->alarmHandle);
  blink->alarmHandle = NULL;

  blink->isVisible = !blink->isVisible;
  setBlinkAlarm(blink);
  scheduleUpdate("blink state changed");
}

static void
setBlinkAlarm (BlinkDescriptor *blink) {
  asyncNewRelativeAlarm(&blink->alarmHandle, getBlinkDuration(blink), handleBlinkAlarm, blink);
}

static void
forEachBlinkDescriptor (void (*handleBlinkDescriptor) (BlinkDescriptor *blink)) {
  BlinkDescriptor *const *blink = blinkDescriptors;

  while (*blink) handleBlinkDescriptor(*blink++);
}

static void
unrequireBlinkDescriptor (BlinkDescriptor *blink) {
  blink->isRequired = 0;
}

void
unrequireAllBlinkDescriptors (void) {
  forEachBlinkDescriptor(unrequireBlinkDescriptor);
}

void
requireBlinkDescriptor (BlinkDescriptor *blink) {
  blink->isRequired = 1;
}

static void
stopBlinkDescriptor (BlinkDescriptor *blink) {
  if (blink->alarmHandle) {
    asyncCancelRequest(blink->alarmHandle);
    blink->alarmHandle = NULL;
  }
}

void
stopAllBlinkDescriptors (void) {
  forEachBlinkDescriptor(stopBlinkDescriptor);
}

static void
resetBlinkDescriptor (BlinkDescriptor *blink) {
  if (!(*blink->isEnabled && blink->isRequired)) {
    stopBlinkDescriptor(blink);
  } else if (!blink->alarmHandle) {
    setBlinkAlarm(blink);
  }
}

void
resetAllBlinkDescriptors (void) {
  forEachBlinkDescriptor(resetBlinkDescriptor);
}
