﻿-----------------------
-- Name: Piano Roll (NES)
-- Author: zeta0134
-- Based on: https://github.com/zeta0134/mesen-piano-roll
-----------------------

local consoleType = emu.getState()["consoleType"]
if consoleType ~= "Nes" then
  emu.displayMessage("Script", "This script only works on the NES.")
  return
end

local NTSC_CPU_FREQUENCY = 1.789773 * 1024.0 * 1024.0
local LOWEST_NOTE_FREQ = 55.0 -- ~A0
local HIGHEST_NOTE_FREQ = 4434.922 -- ~C#8
local PIANO_ROLL_WIDTH = 240
local PIANO_ROLL_KEYS = 76
local PIANO_ROLL_KEY_HEIGHT = 2
local PIANO_ROLL_HEIGHT = PIANO_ROLL_KEYS * PIANO_ROLL_KEY_HEIGHT
local NOISE_KEY_HEIGHT = 2
local NOISE_ROLL_HEIGHT = NOISE_KEY_HEIGHT * 16
local NOISE_ROLL_OFFSET = 200
local SQUARE1_COLORS = {0xFFA0A0, 0xFF40FF, 0xFF4040}
SQUARE1_COLORS[4] = SQUARE1_COLORS[2]
local SQUARE2_COLORS = {0xFFE0A0, 0xFFC040, 0xFFFF40}
SQUARE2_COLORS[4] = SQUARE2_COLORS[2]
local TRIANGLE_COLORS = {0x40FF40}
local NOISE_COLORS = {0x4040FF, 0x40FFFF}
local DMC_BASE_COLOR = 0x806040A0
local DMC_PLAYING_COLOR = 0x8040C0
local DMC_OFFSET = 178
local DMC_HEIGHT = 19
local BACKGROUND_COLOR = 0x80000000
local PIANO_STRING_BLACK_COLOR = 0x80101010
local PIANO_STRING_WHITE_COLOR = 0x80060606
local NOISE_STRING_BLACK_COLOR = 0x80060606
local NOISE_STRING_WHITE_COLOR = 0x800A0A0A

local settings = {
  background="solid"
}

local shadow_apu = {}
for address = 0x4000, 0x4017 do
  shadow_apu[address] = 0
end

local shadow_triangle = {}
shadow_triangle.lc_reload = false
shadow_triangle.lc_value = 0

function clock_linear_counter()
  if shadow_triangle.lc_reload then
    shadow_triangle.lc_value = shadow_apu[0x4008] & 0x7F
  else
    if shadow_triangle.lc_value > 0 then
      shadow_triangle.lc_value = shadow_triangle.lc_value - 1
    end
  end
  if shadow_apu[0x4008] & 0x80 == 0 then
    shadow_triangle.lc_reload = false
  end
end

function clock_shadow_frame_sequencer()
  clock_linear_counter()
  clock_linear_counter()
  clock_linear_counter()
  clock_linear_counter()
end

function apu_register_write(address, value)
  shadow_apu[address] = value
  if address == 0x400B then
    shadow_triangle.lc_reload = true
  end
end

emu.addEventCallback(clock_shadow_frame_sequencer, emu.eventType.endFrame)
emu.addMemoryCallback(apu_register_write, emu.callbackType.write, 0x4000 , 0x4017)

function set_background()
  if settings.background == "solid" then
    BACKGROUND_COLOR = 0x000000
    PIANO_STRING_BLACK_COLOR = 0x101010
    PIANO_STRING_WHITE_COLOR = 0x060606
    NOISE_STRING_BLACK_COLOR = 0x060606
    NOISE_STRING_WHITE_COLOR = 0x0A0A0A
    DMC_BASE_COLOR = 0x006040A0
  end
  if settings.background == "transparent" then
    BACKGROUND_COLOR = 0x80000000
    PIANO_STRING_BLACK_COLOR = 0x80101010
    PIANO_STRING_WHITE_COLOR = 0x80060606
    NOISE_STRING_BLACK_COLOR = 0x80060606
    NOISE_STRING_WHITE_COLOR = 0x800A0A0A
    DMC_BASE_COLOR = 0x806040A0
  end
  if settings.background == "clear" then
    BACKGROUND_COLOR = 0xFF000000
    PIANO_STRING_BLACK_COLOR = 0xFF000000
    PIANO_STRING_WHITE_COLOR = 0xFF000000
    NOISE_STRING_BLACK_COLOR = 0xFF060606
    NOISE_STRING_WHITE_COLOR = 0xFF0A0A0A
    DMC_BASE_COLOR = 0x806040A0
  end
end
-- call this once to initialize
set_background()

function toggle_background()
  if settings.background == "clear" then
    settings.background = "transparent"
  elseif settings.background == "transparent" then
    settings.background = "solid"
  elseif settings.background == "solid" then
    settings.background = "clear"
  end
  set_background()
end

function tiny_a(x, y, color)
  emu.drawLine(x,y+1,x, y+4, color)
  emu.drawLine(x+2,y+1,x+2, y+4, color)
  emu.drawPixel(x+1,y,color)
  emu.drawPixel(x+1,y+2,color)
end

function tiny_b(x, y, color)
  emu.drawLine(x, y, x, y+4, color)
  emu.drawPixel(x+1, y, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawPixel(x+1, y+4, color)
  emu.drawPixel(x+2, y+1, color)
  emu.drawPixel(x+2, y+3, color)
end

function tiny_c(x, y, color)
  emu.drawLine(x, y+1, x, y+3, color)
  emu.drawLine(x+1, y, x+2, y, color)
  emu.drawLine(x+1, y+4, x+2, y+4, color)
end

function tiny_d(x, y, color)
  emu.drawLine(x, y, x, y+4, color)
  emu.drawPixel(x+1, y, color)
  emu.drawPixel(x+1, y+4, color)
  emu.drawLine(x+2, y+1, x+2, y+3,color)
end

function tiny_e(x, y, color)
  emu.drawLine(x, y, x, y+4, color)
  emu.drawLine(x+1, y, x+2, y, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawLine(x+1, y+4, x+2, y+4, color)
end

function tiny_f(x, y, color)
  emu.drawLine(x, y, x, y+4, color)
  emu.drawLine(x+1, y, x+2, y, color)
  emu.drawPixel(x+1, y+2, color)
end

function tiny_g(x, y, color)
  emu.drawLine(x, y+1, x, y+3, color)
  emu.drawLine(x+1, y, x+2, y, color)
  emu.drawLine(x+1, y+4, x+2, y+4, color)
  emu.drawLine(x+2, y+2, x+2, y+3, color)
end

function tiny_h(x, y, color)
  emu.drawLine(x, y, x, y+4, color)
  emu.drawLine(x+2, y, x+2, y+4, color)
  emu.drawPixel(x+1, y+2, color)
end

function tiny_i(x, y, color)
  emu.drawLine(x, y, x+2, y, color)
  emu.drawLine(x, y+4, x+2, y+4, color)
  emu.drawLine(x+1, y+1, x+1, y+3, color)
end

function tiny_j(x, y, color)
  emu.drawLine(x, y, x+2, y, color)
  emu.drawLine(x+2, y+1, x+2, y+3, color)
  emu.drawPixel(x+1, y+4, color)
  emu.drawPixel(x, y+3, color)
end

function tiny_k(x, y, color)
  emu.drawLine(x, y, x, y+4, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawLine(x+2, y, x+2, y+1, color)
  emu.drawLine(x+2, y+3, x+2, y+4, color)
end

function tiny_l(x, y, color)
  emu.drawLine(x, y, x, y+4, color)  
  emu.drawLine(x+1, y+4, x+2, y+4, color)
end

function tiny_m(x, y, color)
  emu.drawLine(x, y, x, y+4, color)  
  emu.drawLine(x+1, y+1, x+1, y+2, color)  
  emu.drawLine(x+2, y, x+2, y+4, color)  
end

function tiny_n(x, y, color)
  emu.drawLine(x, y, x, y+4, color)  
  emu.drawPixel(x+1, y, color)  
  emu.drawLine(x+2, y+1, x+2, y+4, color)  
end

function tiny_o(x, y, color)
  emu.drawLine(x, y+1, x, y+3, color)
  emu.drawLine(x+2, y+1, x+2, y+3, color)
  emu.drawPixel(x+1, y, color)
  emu.drawPixel(x+1, y+4, color)
end

function tiny_p(x, y, color)
  emu.drawLine(x, y, x, y+4, color)  
  emu.drawPixel(x+1, y, color)
  emu.drawPixel(x+2, y+1, color)
  emu.drawPixel(x+1, y+2, color)
end

function tiny_q(x, y, color)
  emu.drawLine(x, y+1, x, y+3, color)
  emu.drawLine(x+2, y+1, x+2, y+3, color)
  emu.drawPixel(x+1, y, color)
  emu.drawPixel(x+1, y+3, color)
  emu.drawLine(x+1, y+4, x+2, y+4, color)
end

function tiny_r(x, y, color)
  emu.drawLine(x, y, x, y+4, color)  
  emu.drawPixel(x+1, y, color)
  emu.drawPixel(x+2, y+1, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawLine(x+2, y+3, x+2, y+4, color)  
end

function tiny_s(x, y, color)
  emu.drawLine(x+1, y, x+2, y, color)  
  emu.drawPixel(x, y+1, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawPixel(x+2, y+3, color)
  emu.drawLine(x, y+4, x+1, y+4, color)  
end

function tiny_t(x, y, color)
  emu.drawLine(x, y, x+2, y, color)  
  emu.drawLine(x+1, y+1, x+1, y+4, color)  
end

function tiny_u(x, y, color)
  emu.drawLine(x, y, x, y+4, color)  
  emu.drawLine(x+2, y, x+2, y+4, color)  
  emu.drawPixel(x+1, y+4, color)
end

function tiny_v(x, y, color)
  emu.drawLine(x, y, x, y+3, color)  
  emu.drawLine(x+2, y, x+2, y+3, color)  
  emu.drawPixel(x+1, y+4, color)
end

function tiny_w(x, y, color)
  emu.drawLine(x, y, x, y+4, color)  
  emu.drawLine(x+1, y+2, x+1, y+3, color)  
  emu.drawLine(x+2, y, x+2, y+4, color)  
end

function tiny_x(x, y, color)
  emu.drawLine(x, y, x, y+1, color)  
  emu.drawLine(x+2, y, x+2, y+1, color)  
  emu.drawPixel(x+1, y+2, color)
  emu.drawLine(x, y+3, x, y+4, color)  
  emu.drawLine(x+2, y+3, x+2, y+4, color)  
end

function tiny_y(x, y, color)
  emu.drawLine(x, y, x, y+2, color)  
  emu.drawLine(x+2, y, x+2, y+2, color)  
  emu.drawLine(x+1, y+2, x+1, y+4, color)
end

function tiny_z(x, y, color)
  emu.drawLine(x, y, x+2, y, color)
  emu.drawLine(x+2,y+1,x,y+3, color)
  emu.drawLine(x,y+4,x+2,y+4, color)
end

function tiny_0(x, y, color)
  emu.drawLine(x, y, x, y+4, color)
  emu.drawLine(x+2, y, x+2, y+4, color)
  emu.drawPixel(x+1, y, color)
  emu.drawPixel(x+1, y+4, color)
end

function tiny_1(x, y, color)
  emu.drawLine(x+1, y, x+1, y+4, color)
  emu.drawLine(x, y+4, x+2, y+4, color)
  emu.drawPixel(x, y+1,color)
end

function tiny_2(x, y, color)
  emu.drawLine(x, y, x+1, y, color)
  emu.drawLine(x+2,y+1,x,y+3, color)
  emu.drawLine(x,y+4,x+2,y+4, color)
end

function tiny_3(x, y, color)
  emu.drawLine(x+2, y, x+2, y+4, color)
  emu.drawLine(x, y, x+1, y, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawLine(x, y+4, x+1, y+4, color)
end

function tiny_4(x, y, color)
  emu.drawLine(x+2, y, x+2, y+4, color)
  emu.drawLine(x, y, x, y+2, color)
  emu.drawPixel(x+1, y+2, color)
end

function tiny_5(x, y, color)
  emu.drawLine(x, y, x+2, y, color)
  emu.drawLine(x, y+1, x, y+2, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawPixel(x+2, y+3, color)
  emu.drawLine(x,y+4,x+1,y+4, color)
end

function tiny_6(x, y, color)
  emu.drawLine(x, y, x, y+4, color)
  emu.drawLine(x+1, y, x+2, y, color)
  emu.drawLine(x+1, y+2, x+2, y+2, color)
  emu.drawLine(x+1, y+4, x+2, y+4, color)
  emu.drawPixel(x+2, y+3, color)
end

function tiny_7(x, y, color)
  emu.drawLine(x+2, y, x+2, y+4, color)
  emu.drawLine(x, y, x+1, y, color)
end

function tiny_8(x, y, color)
  emu.drawLine(x, y, x, y+4, color)
  emu.drawLine(x+2, y, x+2, y+4, color)
  emu.drawPixel(x+1, y, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawPixel(x+1, y+4, color)
end

function tiny_9(x, y, color)
  emu.drawLine(x+2, y, x+2, y+4, color)
  emu.drawLine(x, y, x+1, y, color)
  emu.drawLine(x, y+2, x+1, y+2, color)
  emu.drawLine(x, y+4, x+1, y+4, color)
  emu.drawPixel(x, y+1, color)
end

function tiny_hex_char(x, y, value, color)
  local hex_functions = {
    tiny_0, tiny_1, tiny_2, tiny_3, tiny_4, tiny_5, tiny_6, tiny_7,
    tiny_8, tiny_9, tiny_a, tiny_b, tiny_c, tiny_d, tiny_e, tiny_f}
  if hex_functions[value+1] then
    hex_functions[value+1](x,y,color)
  end
end

function tiny_hex(x, y, value, color, width)
  while width > 0 do
    width = width - 1
    tiny_hex_char(x + width * 4, y, value & 0xF, color)
    value = value >> 4
  end
end

local char_functions = {
  tiny_a, tiny_b, tiny_c, tiny_d, tiny_e, tiny_f,
  tiny_g, tiny_h, tiny_i, tiny_j, tiny_k, tiny_l,
  tiny_m, tiny_n, tiny_o, tiny_p, tiny_q, tiny_r,
  tiny_s, tiny_t, tiny_u, tiny_v, tiny_w, tiny_x,
  tiny_y, tiny_z
}

local num_functions = {
  tiny_0, tiny_1, tiny_2, tiny_3, tiny_4,
  tiny_5, tiny_6, tiny_7, tiny_8, tiny_9
}

function tiny_char(x, y, character_code, color)
  if character_code >= string.byte("a", 1) and character_code <= string.byte("z") then
    char_functions[character_code - string.byte("a") + 1](x, y, color)
  end
  if character_code >= string.byte("A", 1) and character_code <= string.byte("Z") then
    char_functions[character_code - string.byte("A") + 1](x, y, color)
  end
  if character_code >= string.byte("0", 1) and character_code <= string.byte("9") then
    num_functions[character_code - string.byte("0") + 1](x, y, color)
  end
end

function tiny_string(x, y, str, color)
  for i = 1, #str do
    tiny_char(x + ((i-1) * 4), y, string.byte(str, i), color)
  end
end

function frequency_to_coordinate(note_frequency, lowest_freq, highest_freq, viewport_height)
  local highest_log = math.log(highest_freq)
  local lowest_log = math.log(lowest_freq)
  local range = highest_log - lowest_log
  local note_log = math.log(note_frequency)

  local coordinate = (note_log - lowest_log) * viewport_height / range
  if coordinate >= 0 and coordinate < viewport_height then
    return coordinate
  else
    return nil
  end
end

function pulse_frequency(pulse_period)
  return NTSC_CPU_FREQUENCY / (16.0 * (pulse_period + 1.0))
end

function triangle_frequency(triangle_period)
  return NTSC_CPU_FREQUENCY / (32.0 * (triangle_period + 1.0))
end

function unpack_argb(color)
  return ((color & 0xFF000000) >> 24), ((color & 0xFF0000) >> 16), ((color & 0xFF00) >> 8), (color & 0xFF)
end

function pack_argb(a, r, g, b)
  return (a << 24) | (r << 16) | (g << 8) | (b)
end

function apply_brightness(color, brightness)
  local a, r, g, b = unpack_argb(color)
  r = math.floor(r * brightness)
  g = math.floor(g * brightness)
  b = math.floor(b * brightness)
  return pack_argb(a, r, g, b)
end

function apply_transparency(color, alpha)
  local a, r, g, b = unpack_argb(color)
  a = math.floor(0xFF * (1.0 - alpha))
  return pack_argb(a, r, g, b)
end

local square1_roll = {}
local square2_roll = {}
local triangle_roll = {}
local noise_roll = {}
local dmc_roll = {}

function initialize_piano_roll()
  for i = 1, PIANO_ROLL_WIDTH - 1 do
    local channel_state = {}
    -- this is enough for pulse, triangle, and noise
    channel_state.enabled = false
    -- dmc calculates a delta, so it needs full dummy samples
    channel_state.playing = false
    channel_state.delta = 0

    table.insert(square1_roll, channel_state)
    table.insert(square2_roll, channel_state)
    table.insert(triangle_roll, channel_state)
    table.insert(noise_roll, channel_state)
    table.insert(dmc_roll, channel_state)
  end
end

initialize_piano_roll()

function update_piano_roll(channel, state_table)
  local channel_state = {}
  local coordinate = frequency_to_coordinate(channel.frequency, LOWEST_NOTE_FREQ, HIGHEST_NOTE_FREQ, PIANO_ROLL_HEIGHT)
  if coordinate then
    channel_state.y = PIANO_ROLL_HEIGHT - coordinate
  end
  if channel.envelope then
    -- pulse channels
    channel_state.volume = channel.envelope.volume
    channel_state.enabled = channel.envelope.volume ~= 0 and channel.lengthCounter.counter > 0
    channel_state.duty = channel.duty
  else
    -- triangle channels
    channel_state.volume = 6
    channel_state.enabled = 
      channel.lengthCounter.counter > 0 and
      shadow_triangle.lc_value > 0 and
      channel.period > 2 and
      channel.enabled
    channel_state.duty = 0
  end
  
  table.insert(state_table, channel_state)
  if #state_table > PIANO_ROLL_WIDTH then
    table.remove(state_table, 1)
  end
end

local noise_period_table = {}
noise_period_table[4]    =  0 
noise_period_table[8]    =  1
noise_period_table[16]   =  2
noise_period_table[32]   =  3
noise_period_table[64]   =  4
noise_period_table[96]   =  5
noise_period_table[128]  =  6
noise_period_table[160]  =  7
noise_period_table[202]  =  8
noise_period_table[254]  =  9
noise_period_table[380]  =  10
noise_period_table[508]  =  11
noise_period_table[762]  =  12
noise_period_table[1016] =  13
noise_period_table[2034] =  14
noise_period_table[4068] =  15

function debug_table_keys(name, t)
  local keys = {}
  for k,v in pairs(t) do
    keys[#keys+1] = k
  end
  emu.log(name..": "..table.concat(keys, ","))
end

function update_noise_roll(channel, state_table)
  local channel_state = {}
  if noise_period_table[channel.period + 1] then
    channel_state.y = noise_period_table[channel.period + 1] * NOISE_KEY_HEIGHT
    channel_state.period = noise_period_table[channel.period + 1]
    channel_state.mode = channel.modeFlag
  end
  if channel.envelope.constantVolume then
    channel_state.volume = channel.envelope.volume
  else 
    channel_state.volume = channel.envelope.counter
  end
  channel_state.enabled = channel_state.volume ~= 0 and channel.lengthCounter.counter > 0

  table.insert(state_table, channel_state)
  if #state_table > PIANO_ROLL_WIDTH then
    table.remove(state_table, 1)
  end
end

local old_dmc_level = 0

local dmc_period_table = {}
dmc_period_table[428] = 0
dmc_period_table[380] = 1
dmc_period_table[340] = 2
dmc_period_table[320] = 3
dmc_period_table[286] = 4
dmc_period_table[254] = 5
dmc_period_table[226] = 6
dmc_period_table[214] = 7
dmc_period_table[190] = 8
dmc_period_table[160] = 9
dmc_period_table[142] = 10
dmc_period_table[128] = 11
dmc_period_table[106] = 12
dmc_period_table[84]  = 13
dmc_period_table[72]  = 14
dmc_period_table[54]  = 15

function update_dmc_roll(channel, state_table)
  local channel_state = {}
  local dmc_playing = channel.bytesRemaining > 0
  local delta = math.abs(channel.outputVolume - old_dmc_level)
  channel_state.playing = dmc_playing
  channel_state.delta = delta
  channel_state.address = channel.sampleAddr
  
  if dmc_period_table[channel.period + 1] then
    channel_state.rate = dmc_period_table[channel.period + 1]
  end
  table.insert(state_table, channel_state)
  if #state_table > PIANO_ROLL_WIDTH then
    table.remove(state_table, 1)
  end
  old_dmc_level = channel.outputVolume
end

function draw_piano_roll(emu, state_table, duty_colors)  
  for x = 1, #state_table do
    if state_table[x].enabled and state_table[x].y then
      local volume = state_table[x].volume
      local y = state_table[x].y - 2
      local foreground = duty_colors[state_table[x].duty + 1]
      -- outer lines, drawn at brightness dependant on volume
      local outermost_volume = math.max(0, volume - 8) / 8
      local outermost_brightness = math.min(1.0, outermost_volume)
      emu.drawLine(x - 1, y-2, x - 1, y+2, apply_transparency(foreground, outermost_brightness))
      local innermost_volume = volume / 8
      local innermost_brightness = math.min(1.0, innermost_volume)
      emu.drawLine(x - 1, y-1, x - 1, y+1, apply_transparency(foreground, innermost_brightness))
      -- center line, always drawn at full brightness when playing
      emu.drawPixel(x - 1, y, foreground)
    end
  end
end

function draw_noise_roll(emu, state_table, duty_colors)  
  for x = 1, #state_table do
    if state_table[x].enabled and state_table[x].y then
      local volume = state_table[x].volume
      local y = state_table[x].y
      local foreground = duty_colors[1]
      if state_table[x].mode then
        foreground = duty_colors[2]
      end
      -- outer lines, drawn at brightness dependant on volume
      local outermost_volume = math.max(0, volume - 8) / 8
      local outermost_brightness = math.min(1.0, outermost_volume)
      emu.drawLine(x - 1, NOISE_ROLL_OFFSET + y-2, x - 1, NOISE_ROLL_OFFSET + y+2, apply_transparency(foreground, outermost_brightness))
      local innermost_volume = volume / 8
      local innermost_brightness = math.min(1.0, innermost_volume)
      emu.drawLine(x - 1, NOISE_ROLL_OFFSET + y-1, x - 1, NOISE_ROLL_OFFSET + y+1, apply_transparency(foreground, innermost_brightness))
      -- center line, always drawn at full brightness when playing
      emu.drawPixel(x - 1, NOISE_ROLL_OFFSET + y, foreground)
    end
  end
end

function draw_piano_background()
   emu.drawRectangle(0, 0, 256, 240, BACKGROUND_COLOR, true)
end

function draw_piano_strings()
  local black_string = PIANO_STRING_BLACK_COLOR
  local white_string = PIANO_STRING_WHITE_COLOR
  local string_colors = {
    white_string, --C
    white_string, --B
    black_string, --Bb
    white_string, --A
    black_string, --Ab
    white_string, --G
    black_string, --Gb
    white_string, --F
    white_string, --E
    black_string, --Eb
    white_string, --D
    black_string, --Db
  }
  
  for i = 0, PIANO_ROLL_KEYS do
    local string_color = string_colors[i%12 + 1]
    local y = i*PIANO_ROLL_KEY_HEIGHT
    emu.drawLine(0, y, PIANO_ROLL_WIDTH, y, string_color)
  end
end

function draw_noise_strings()
  local noise_string_colors = {
    NOISE_STRING_BLACK_COLOR,
    NOISE_STRING_WHITE_COLOR
  }
  for i = 0, 15 do
    local y = i * NOISE_KEY_HEIGHT
    emu.drawLine(0, NOISE_ROLL_OFFSET + y, PIANO_ROLL_WIDTH, NOISE_ROLL_OFFSET + y, noise_string_colors[(i%2)+1])
  end
end

local white_key_border = 0x404040
local white_key = 0x505050
local black_key = 0x000000
local black_key_border = 0x181818
local upper_key_pixels = {
  white_key, -- C
  white_key_border, 
  white_key, -- B
  black_key, black_key, black_key, -- Bb
  white_key, -- A
  black_key, black_key, black_key, -- Ab
  white_key, -- G
  black_key, black_key, black_key, -- Gb
  white_key, -- F
  white_key_border,
  white_key, -- E
  black_key, black_key, black_key, -- Eb
  white_key, -- D
  black_key, black_key, black_key, -- Db
}
  
local lower_key_pixels = {
  white_key, -- C (bottom half)
  white_key_border,
  white_key, white_key, -- B
  white_key_border, 
  white_key, white_key, white_key, -- A
  white_key_border, 
  white_key, white_key, white_key, -- G
  white_key_border,
  white_key, white_key, -- F
  white_key_border,
  white_key, white_key, -- E
  white_key_border, 
  white_key, white_key, white_key, -- D
  white_key_border,
  white_key, -- C (top half)
}

function draw_piano_keys()
  -- first, draw the keys themselves
  for y = 0, PIANO_ROLL_KEYS * PIANO_ROLL_KEY_HEIGHT do
    local upper_key_color = upper_key_pixels[y % #upper_key_pixels + 1]
    local lower_key_color = lower_key_pixels[y % #lower_key_pixels + 1]
    emu.drawLine(240, y, 248, y, upper_key_color)
    emu.drawLine(248, y, 256, y, lower_key_color)
  end
  
  -- now clean up the border between the key and the piano roll
  emu.drawLine(240, 0, 240, PIANO_ROLL_HEIGHT, black_key_border)
end

function draw_right_white_key(y, color)  
  emu.drawLine(248, y + 1, 256, y + 1, color)
  emu.drawLine(240, y, 256, y, color)
end

function draw_center_white_key(y, color)
  emu.drawLine(248, y - 1, 256, y - 1, color)
  emu.drawLine(240, y, 256, y, color)
  emu.drawLine(248, y + 1, 256, y + 1, color)
end

function draw_left_white_key(y, color)
  emu.drawLine(240, y, 256, y, color)
  emu.drawLine(248, y - 1, 256, y - 1, color)
end

function draw_black_key(y, color)
  emu.drawLine(241, y - 1, 247, y - 1, color)
  emu.drawLine(241, y, 247, y, color)
  emu.drawLine(241, y + 1, 247, y + 1, color)
end

function draw_key_spot(note, duty_colors)
  if (note.y == nil or note.enabled == false) then
    return
  end
  local key_drawing_functions = {
    draw_left_white_key,  --C
    draw_right_white_key, --B
    draw_black_key,       --Bb
    draw_center_white_key,--A
    draw_black_key,       --Ab
    draw_center_white_key,--G
    draw_black_key,       --Gb
    draw_left_white_key,  --F
    draw_right_white_key, --E
    draw_black_key,       --Eb
    draw_center_white_key,--D
    draw_black_key,       --Db
  }

  local base_color = duty_colors[note.duty + 1]

  -- Calculate two key spots, based on tuning. This helps to
  -- highlight intentionally out of tune notes, and lights up the closest
  -- piano keys relative to standard tuning. The brightness of each pad is
  -- relative to the tuning offset, so the brighter key is (usually) the
  -- real note
  
  local note_key = (note.y - 2) / PIANO_ROLL_KEY_HEIGHT
  local base_key = math.floor(note_key)
  local base_percent = 1.0 - (note_key % 1)
  local adjacent_key = math.ceil(note_key)
  local adjacent_percent = note_key % 1

  -- Now take both of these colors and apply some transparency based on volume
  local volume_percent = (note.volume / 15) * 0.75 + 0.25
  base_percent = base_percent * volume_percent
  adjacent_percent = adjacent_percent * volume_percent

  local base_y = base_key * PIANO_ROLL_KEY_HEIGHT
  local base_key_color = apply_transparency(base_color, base_percent)
  
  local adjacent_y = adjacent_key * PIANO_ROLL_KEY_HEIGHT
  local adjacent_key_color = apply_transparency(base_color, adjacent_percent)

  key_drawing_functions[base_key % 12 + 1](base_y, base_key_color)
  key_drawing_functions[adjacent_key % 12 + 1](adjacent_y, adjacent_key_color)
end

function draw_pad(x, y, value, foreground, background)
  emu.drawRectangle(x, y-1, 4, 9, background, true)
  emu.drawLine(x+4, y, x+4, y+6, background)
  tiny_hex_char(x+1,y+1,value,foreground)
end

function draw_noise_pads(active_note, duty_colors)
  local pad_colors = {
    0x101010,
    0x121212,
    0x141414,
    0x161616,
    0x181818,
    0x1A1A1A,
    0x1C1C1C,
    0x1E1E1E,
    0x202020,
    0x222222,
    0x242424,
    0x262626,
    0x282828,
    0x2A2A2A,
    0x2C2C2C,
    0x2E2E2E,
  }

  for i = 0, 15 do
    local x = 240 + ((i % 4) * 4) 
    local y = i * 2 + NOISE_ROLL_OFFSET - 3
    local color = pad_colors[(0xF - i) + 1]
    local hex_value = 0xF - i;
    if (active_note.y and active_note.enabled and active_note.period == i) then
      color = duty_colors[1]
      if active_note.mode then
        color = duty_colors[2]
      end
    end
    draw_pad(x, y, hex_value, apply_brightness(color, 1.8), color)
  end

  emu.drawLine(240, NOISE_ROLL_OFFSET - 4, 240, NOISE_ROLL_OFFSET + 30, 0x101010)
  emu.drawLine(241, NOISE_ROLL_OFFSET + 30, 243, NOISE_ROLL_OFFSET + 30, NOISE_STRING_BLACK_COLOR)
end

function draw_dmc_roll(emu, state_table)
  for x = 1, #state_table do
    local y = DMC_OFFSET
    local color = DMC_PLAYING_COLOR
    if not state_table[x].playing then
      color = DMC_BASE_COLOR
    end
    -- width of "sample" based on delta, maximum is 127
    local sample_offset = math.floor((state_table[x].delta / 127) * DMC_HEIGHT)
    emu.drawLine(x - 1, y - sample_offset, x - 1, y + sample_offset, color)
  end
end

function draw_dmc_head(note)
  local foreground = 0x404040
  local background = 0x202020
  if note.playing then
    foreground = DMC_PLAYING_COLOR
    background = apply_brightness(DMC_PLAYING_COLOR, 0.5)
  end
  emu.drawRectangle(242, DMC_OFFSET - 6, 13, 13, background, true)
  emu.drawRectangle(241, DMC_OFFSET - 5, 15, 11, background, true)
  emu.drawLine(240, DMC_OFFSET - 3, 240, DMC_OFFSET + 3, 0x101010)
  tiny_hex(241, DMC_OFFSET - 2, note.address, foreground, 4)
end

local old_mouse_state = {}

function is_region_clicked(x ,y, width, height, mouse_state)
  if mouse_state.left and not old_mouse_state.left then
    if mouse_state.x >= x and mouse_state.x < x + width and mouse_state.y >= y and mouse_state.y < y + height then
      return true
    end
  end
  return false
end

function handle_input()
  local mouse_state = emu.getMouseState()
  if is_region_clicked(0, 0, 256, 240, mouse_state) then
    toggle_background()
  end
  old_mouse_state = mouse_state
end

-- drawing functions for various icons in the register display
function draw_raw_registers(x, y, box_color, shadow_color, numeral_color, background_color, values)
  emu.drawRectangle(x+1, y+1, 36, 7, shadow_color)
  emu.drawRectangle(x, y, 36, 7, box_color, true)
  for i = 1, 4 do
    emu.drawRectangle(x + ((i - 1) * 9) + 1, y + 1, 7, 5, background_color, true)
    if values[i] ~= nil then
      tiny_hex(x + ((i - 1) * 9) + 1, y + 1, values[i], numeral_color, 2)
    end
  end
end

function draw_duty_12(x, y, color)
  emu.drawLine(x, y+3, x+4, y+3, color)
  emu.drawLine(x+1, y+1, x+1, y+2, color)
end

function draw_duty_25(x, y, color)
  emu.drawLine(x, y+3, x+1, y+3, color)
  emu.drawLine(x+3, y+3, x+4, y+3, color)
  emu.drawLine(x+1, y+1, x+3, y+1, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawPixel(x+3, y+2, color)
end

function draw_duty_50(x, y, color)
  emu.drawLine(x, y+3, x+2, y+3, color)
  emu.drawLine(x+2, y+1, x+4, y+1, color)
  emu.drawPixel(x+2, y+2, color)
end

function draw_duty_75(x, y, color)
  emu.drawLine(x, y+1, x+1, y+1, color)
  emu.drawLine(x+3, y+1, x+4, y+1, color)
  emu.drawLine(x+1, y+3, x+3, y+3, color)
  emu.drawPixel(x+1, y+2, color)
  emu.drawPixel(x+3, y+2, color)
end

local duty_icon_functions = {draw_duty_12, draw_duty_25, draw_duty_50, draw_duty_75}

function draw_duty_indicator(x, y, icon_color, box_color, shadow_color, light_color, dark_color, selected_light_color, selected_dark_color, duty_cycle, playing)
  local icon_color = light_color
  if playing then
    icon_color = selected_light_color
  end
  emu.drawLine(x, y+6, x+2, y+6, icon_color)
  emu.drawLine(x+2, y, x+2, y+5, icon_color)
  emu.drawLine(x+3, y, x+6, y, icon_color)
  emu.drawLine(x+7, y, x+7, y+5, icon_color)
  emu.drawLine(x+7, y+6, x+9, y+6, icon_color)

  emu.drawRectangle(x+12, y+1, 25, 7, shadow_color)
  emu.drawRectangle(x+11, y, 25, 7, box_color, true)

  for i = 1, 4 do
    if playing and duty_cycle == (i - 1) then
      emu.drawRectangle(x + ((i - 1) * 6) + 12, y + 1, 5, 5, selected_dark_color, true)
      duty_icon_functions[i](x + ((i - 1) * 6) + 12, y + 1, selected_light_color)
    else
      emu.drawRectangle(x + ((i - 1) * 6) + 12, y + 1, 5, 5, dark_color, true)
      duty_icon_functions[i](x + ((i - 1) * 6) + 12, y + 1, light_color)
    end
  end
end

function draw_sweep_indicator(x, y, icon_color, box_color, shadow_color, light_color, dark_color, selected_light_color, selected_dark_color, sweep_byte)
  local negate = ((sweep_byte & 0x40) ~= 0)
  local period = ((sweep_byte & 0x70) >> 4)
  local shift = ((sweep_byte & 0x07))
  local enabled = ((sweep_byte & 0x80) ~= 0) and (shift ~= 0)

  local icon_color = light_color
  if enabled then
    icon_color = selected_light_color
  end

  -- sweep icon
  emu.drawLine(x, y, x+6, y+6, icon_color)
  emu.drawLine(x+3, y+4, x+5, y+6, icon_color)
  emu.drawLine(x+3, y+5, x+4, y+6, icon_color)
  emu.drawLine(x+4, y+3, x+6, y+5, icon_color)
  emu.drawLine(x+7, y+4, x+7, y+5, icon_color)
  emu.drawPixel(x+9, y+4, icon_color)
  emu.drawPixel(x+8, y+2, icon_color)

  -- bar graph indicators
  emu.drawPixel(x+21, y, icon_color)
  emu.drawPixel(x+22, y+1, icon_color)
  emu.drawPixel(x+21, y+2, icon_color)
  emu.drawPixel(x+24, y, icon_color)
  emu.drawPixel(x+25, y+1, icon_color)
  emu.drawPixel(x+24, y+2, icon_color)

  emu.drawPixel(x+21, y+5, icon_color)
  emu.drawLine(x+22, y+4, x+22, y+6, icon_color)
  emu.drawLine(x+24, y+4, x+24, y+6, icon_color)
  emu.drawPixel(x+25, y+5, icon_color)

  -- indicator outlines
  emu.drawRectangle(x+12, y+1, 7, 7, shadow_color)
  emu.drawRectangle(x+11, y, 7, 7, box_color, true)

  emu.drawRectangle(x+28, y+1, 9, 7, shadow_color)
  emu.drawRectangle(x+27, y, 9, 7, box_color, true)

  if enabled then
    emu.drawRectangle(x+12, y+1, 5, 5, selected_dark_color, true)
    emu.drawLine(x+13, y+3, x+15, y+3, selected_light_color)
    if negate then
      emu.drawPixel(x+14, y+2, selected_light_color)
      emu.drawLine(x+12, y+4, x+16, y+4, selected_light_color)
    else
      emu.drawPixel(x+14, y+4, selected_light_color)
      emu.drawLine(x+12, y+2, x+16, y+2, selected_light_color)
    end
    emu.drawRectangle(x+28, y+1, 7, 2, selected_dark_color)
    emu.drawRectangle(x+28, y+4, 7, 2, selected_dark_color)
    emu.drawRectangle(x+28, y+1, shift, 2, selected_light_color)
    emu.drawRectangle(x+28, y+4, period, 2, selected_light_color)
  else
    -- simply blank out the indicators and draw nothing
    emu.drawRectangle(x+12, y+1, 5, 5, dark_color, true)
    emu.drawRectangle(x+28, y+1, 7, 2, dark_color)
    emu.drawRectangle(x+28, y+4, 7, 2, dark_color)
  end
end

function draw_volume_envelope_indicator(x, y, icon_color, box_color, shadow_color, light_color, dark_color, selected_light_color, selected_dark_color, volume_byte)
  local constant_volume = ((volume_byte & 0x10) ~= 0)

  local icon_color = light_color
  if not constant_volume then
    icon_color = selected_light_color
  end

  -- volume icon
  emu.drawLine(x, y+2, x, y+4, icon_color)
  emu.drawPixel(x+1, y+2, icon_color)
  emu.drawPixel(x+1, y+4, icon_color)
  emu.drawLine(x+2, y+1, x+2, y+5, icon_color)
  emu.drawLine(x+3, y, x+3, y+1, icon_color)
  emu.drawLine(x+3, y+5, x+3, y+6, icon_color)
  emu.drawPixel(x+4, y, icon_color)
  emu.drawPixel(x+4, y+6, icon_color)
  emu.drawLine(x+5, y, x+5, y+6, icon_color)
  emu.drawLine(x+7, y+2, x+7, y+4, icon_color)
  emu.drawLine(x+7, y, x+8, y, icon_color)
  emu.drawLine(x+7, y+6, x+8, y+6, icon_color)
  emu.drawLine(x+9, y+1, x+9, y+5, icon_color)

  -- box backgrounds and scaffolding
  emu.drawRectangle(x+12, y+1, 13, 7, shadow_color)
  emu.drawRectangle(x+11, y, 13, 7, box_color, true)

  emu.drawLine(x+25, y+4, x+32, y+4, shadow_color)
  emu.drawLine(x+24, y+3, x+31, y+3, box_color)

  emu.drawRectangle(x+32, y+1, 5, 7, shadow_color)
  emu.drawRectangle(x+31, y, 5, 7, box_color, true)

  if constant_volume then
    -- draw un-highlighted icons
    emu.drawRectangle(x+12, y+1, 5, 5, dark_color, true)
    emu.drawRectangle(x+12, y+1, 2, 5, light_color, true)
    emu.drawRectangle(x+14, y+2, 2, 3, light_color, true)
    emu.drawPixel(x+16, y+3, light_color)

    emu.drawRectangle(x+18, y+1, 5, 5, dark_color, true)
    emu.drawPixel(x+19, y+2, light_color)
    emu.drawPixel(x+19, y+4, light_color)
    emu.drawLine(x+21, y+1, x+21, y+5, light_color)

    -- do not draw envelope period, but blank the cell
    emu.drawRectangle(x+32, y+1, 3, 5, dark_color, true)
  else
    -- draw uhighlighted icons
    emu.drawRectangle(x+12, y+1, 5, 5, selected_dark_color, true)
    emu.drawRectangle(x+12, y+1, 2, 5, selected_light_color, true)
    emu.drawRectangle(x+14, y+2, 2, 3, selected_light_color, true)
    emu.drawPixel(x+16, y+3, selected_light_color) 

    local loop = (volume_byte & 0x30) ~= 0
    if loop then
      emu.drawRectangle(x+18, y+1, 5, 5, selected_dark_color, true)
      emu.drawPixel(x+19, y+2, selected_light_color)
      emu.drawPixel(x+19, y+4, selected_light_color)
      emu.drawLine(x+21, y+1, x+21, y+5, selected_light_color)
    else
      emu.drawRectangle(x+18, y+1, 5, 5, dark_color, true)
      emu.drawPixel(x+19, y+2, light_color)
      emu.drawPixel(x+19, y+4, light_color)
      emu.drawLine(x+21, y+1, x+21, y+5, light_color)
    end

    -- populate the envelope period
    local period = (volume_byte & 0xF)
    emu.drawRectangle(x+32, y+1, 3, 5, selected_dark_color, true)
    tiny_hex(x+32, y+1, period, selected_light_color, 1)
  end
end

function draw_volume_bar(x, y, icon_color, box_color, shadow_color, light_color, dark_color, selected_light_color, selected_dark_color, volume_byte, current_volume)
  emu.drawRectangle(x+1, y+1, 36, 7, shadow_color)
  emu.drawRectangle(x, y, 36, 7, box_color, true)

  -- first, handle the constant volume indicator
  local constant_volume = ((volume_byte & 0x10) ~= 0)
  if constant_volume then
    emu.drawRectangle(x+1, y+1, 3, 5, selected_dark_color, true)
    tiny_hex(x+1, y+1, current_volume, selected_light_color, 1)
  else
    emu.drawRectangle(x+1, y+1, 3, 5, dark_color, true)
  end

  -- then, based on the current playing volume, update the bar. Note this is the output volume, NOT the
  -- value written to the register. It should follow the active envelope.
  for i = 1, 15 do
    local dx = x+5 + ((i-1) * 2)
    local dy = y+1
    if current_volume >= i then
      emu.drawLine(dx, dy, dx, dy+4, selected_dark_color)
      emu.drawLine(dx+1, dy, dx+1, dy+4, selected_light_color)
    else
      emu.drawLine(dx, dy, dx, dy+4, dark_color)
      emu.drawLine(dx+1, dy, dx+1, dy+4, light_color)
    end
  end
end

function draw_triangle_indicator(x, y, icon_color, box_color, shadow_color, light_color, dark_color, selected_light_color, selected_dark_color, triangle_playing)
  local icon_color = light_color
  if triangle_playing then
    icon_color = selected_light_color
  end

  emu.drawLine(x, y+5, x+4, y+1, icon_color)
  emu.drawLine(x+5, y+1, x+9, y+5, icon_color)

  emu.drawRectangle(x+12, y+1, 25, 7, shadow_color)
  emu.drawRectangle(x+11, y, 25, 7, box_color, true)

  if triangle_playing then
    emu.drawRectangle(x+12, y+1, 23, 5, selected_dark_color, true)
    emu.drawRectangle(x+21, y+1, 2, 5, selected_light_color)
    emu.drawRectangle(x+23, y+2, 2, 3, selected_light_color)
    emu.drawLine(x+25, y+3, x+26, y+3, selected_light_color)
  else
    emu.drawRectangle(x+12, y+1, 23, 5, dark_color, true) 
    emu.drawRectangle(x+21, y+1, 2, 5, light_color)
    emu.drawRectangle(x+23, y+2, 2, 3, light_color)
    emu.drawLine(x+25, y+3, x+26, y+3, light_color)
  end
end

function draw_mode_0(x, y, color)
  emu.drawPixel(x, y+2, color)
  emu.drawPixel(x+1, y, color)
  emu.drawPixel(x+2, y+3, color)
  emu.drawPixel(x+3, y+4, color)
  emu.drawPixel(x+4, y+2, color)
  emu.drawPixel(x+5, y, color)
  emu.drawPixel(x+6, y+2, color)
  emu.drawPixel(x+7, y+3, color)
  emu.drawPixel(x+8, y+1, color)
  emu.drawPixel(x+9, y+4, color)
  emu.drawPixel(x+10, y, color)
end

function draw_mode_1(x, y, color)
  emu.drawPixel(x, y+2, color)
  emu.drawLine(x+1, y+1, x+1, y+2, color)
  emu.drawPixel(x+2, y, color)
  emu.drawPixel(x+3, y+4, color)
  emu.drawPixel(x+4, y+3, color)
  emu.drawLine(x+5, y+2, x+5, y+3, color)
  emu.drawPixel(x+6, y+1, color)
  emu.drawPixel(x+7, y, color)
  emu.drawLine(x+8, y+3, x+8, y+4, color)
  emu.drawPixel(x+9, y+2, color)
  emu.drawPixel(x+10, y+1, color)
end

local mode_functions = {}
mode_functions[0] = draw_mode_0
mode_functions[1] = draw_mode_1

function draw_noise_mode(x, y, icon_color, box_color, shadow_color, light_color, dark_color, selected_light_color, selected_dark_color, noise_byte, playing)
  local icon_color = light_color
  if playing then
    icon_color = selected_light_color
  end

  emu.drawPixel(x, y+3, icon_color)
  emu.drawPixel(x+1, y+2, icon_color)
  emu.drawLine(x+2, y+3, x+2, y+5, icon_color)
  emu.drawLine(x+3, y, x+3, y+2, icon_color)
  emu.drawLine(x+4, y+3, x+4, y+4, icon_color)
  emu.drawPixel(x+5, y+2, icon_color)
  emu.drawLine(x+6, y+3, x+6, y+6, icon_color)
  emu.drawLine(x+7, y+1, x+7, y+2, icon_color)
  emu.drawLine(x+8, y+3, x+9, y+3, icon_color)

  emu.drawRectangle(x+12, y+1, 25, 7, shadow_color)
  emu.drawRectangle(x+11, y, 25, 7, box_color, true)

  local noise_mode = ((noise_byte & 0x80) >> 7)
  for i = 0, 1 do
    if i == noise_mode then
      emu.drawRectangle(x+12 + (i*12), y+1, 11, 5, selected_dark_color, true)
      mode_functions[i](x+12 + (i*12), y+1, selected_light_color)
    else
      emu.drawRectangle(x+12 + (i*12), y+1, 11, 5, dark_color, true)
      mode_functions[i](x+12 + (i*12), y+1, light_color)
    end
  end
end

function draw_dpcm_sample_indicator(x, y, icon_color, box_color, shadow_color, light_color, dark_color, selected_light_color, selected_dark_color, frequency_byte, sample_address_byte, dpcm_active)
  local icon_color = light_color
  if dpcm_active then
    icon_color = selected_light_color
  end

  emu.drawLine(x, y+3, x+9, y+3, icon_color)
  emu.drawLine(x+1, y+2, x+1, y+4, icon_color)
  emu.drawLine(x+2, y+1, x+2, y+5, icon_color)
  emu.drawLine(x+4, y+2, x+4, y+4, icon_color)
  emu.drawLine(x+6, y+1, x+6, y+5, icon_color)
  emu.drawLine(x+7, y, x+7, y+6, icon_color)
  emu.drawLine(x+8, y+2, x+8, y+4, icon_color)

  emu.drawRectangle(x+13, y+1, 5, 7, shadow_color)
  emu.drawRectangle(x+12, y, 5, 7, box_color, true)

  emu.drawLine(x+18, y+4, x+19, y+4, shadow_color)
  emu.drawLine(x+17, y+3, x+18, y+3, box_color)

  emu.drawRectangle(x+20, y+1, 17, 7, shadow_color)
  emu.drawRectangle(x+19, y, 17, 7, box_color, true)

  local frequency = frequency_byte & 0xF
  local address = 0xC000 + (sample_address_byte << 6)

  if dpcm_active then
    emu.drawRectangle(x+13, y+1, 3, 5, selected_dark_color, true)
    emu.drawRectangle(x+20, y+1, 15, 5, selected_dark_color, true)
    tiny_hex(x+13, y+1, frequency, selected_light_color, 1)
    tiny_hex(x+20, y+1, address, selected_light_color, 4)
  else
    emu.drawRectangle(x+13, y+1, 3, 5, dark_color, true)
    emu.drawRectangle(x+20, y+1, 15, 5, dark_color, true)
    tiny_hex(x+13, y+1, frequency, light_color, 1)
    tiny_hex(x+20, y+1, address, light_color, 4)
  end
end

function draw_dpcm_length_indicator(x, y, icon_color, box_color, shadow_color, light_color, dark_color, selected_light_color, selected_dark_color, control_byte, length_byte, dpcm_active)
  local icon_color = light_color
  if dpcm_active then
    icon_color = selected_light_color
  end

  emu.drawLine(x+3, y, x+5, y, icon_color)
  emu.drawLine(x+7, y+2, x+7, y+4, icon_color)
  emu.drawLine(x+3, y+6, x+5, y+6, icon_color)
  emu.drawLine(x+1, y+2, x+1, y+4, icon_color)
  emu.drawPixel(x+2,y+1, icon_color)
  emu.drawPixel(x+6,y+1, icon_color)
  emu.drawPixel(x+2,y+5, icon_color)
  emu.drawPixel(x+6,y+5, icon_color)
  emu.drawLine(x+4, y+1, x+4, y+3, icon_color)
  emu.drawLine(x+5, y+3, x+6, y+3, icon_color)

  emu.drawRectangle(x+18, y+1, 19, 7, shadow_color)
  emu.drawRectangle(x+17, y, 19, 7, box_color, true)

  local looping = ((control_byte & 0x40) ~= 0)
  local sample_length = (length_byte * 16) + 1

  if dpcm_active then
    emu.drawRectangle(x+18,y+1,11, 5, selected_dark_color, true)
    emu.drawRectangle(x+30,y+1,5, 5, selected_dark_color, true)
    tiny_hex(x+18,y+1,sample_length, selected_light_color, 3)
    emu.drawPixel(x+31, y+2, selected_light_color)
    emu.drawPixel(x+31, y+4, selected_light_color)
    emu.drawLine(x+33, y+1, x+33, y+5, selected_light_color)
  else
    emu.drawRectangle(x+18,y+1,11, 5, dark_color, true)
    emu.drawRectangle(x+30,y+1,5, 5, dark_color, true)
    tiny_hex(x+18,y+1,sample_length, light_color, 3)
    emu.drawPixel(x+31, y+2, light_color)
    emu.drawPixel(x+31, y+4, light_color)
    emu.drawLine(x+33, y+1, x+33, y+5, light_color)
  end
end

local ICON_COLOR = 0x808080
local SHADOW_COLOR = 0x80000000
local BOX_OUTLINE_COLOR = 0x000000
local UNSELECTED_DARK_COLOR = 0x404040
local UNSELECTED_LIGHT_COLOR = 0x808080

function draw_apu_registers()
  emu.drawRectangle(0, 0, 39, 240, 0x40202020, true)

  local square1_duty = (shadow_apu[0x4000] & 0xC0) >> 6
  local square1_selected_light = SQUARE1_COLORS[square1_duty + 1]
  local square1_selected_dark = square1_selected_light + 0xB0000000

  tiny_string(1, 1, "Pulse 1", 0xFFFFFF)
  draw_raw_registers(1, 7, BOX_OUTLINE_COLOR, SHADOW_COLOR, UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR,
    {shadow_apu[0x4000],shadow_apu[0x4001],shadow_apu[0x4002],shadow_apu[0x4003]})
  draw_duty_indicator(1, 16, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    square1_selected_light, square1_selected_dark, -- icon color when highlighted
    (shadow_apu[0x4000] & 0xC0) >> 6, square1_roll[#square1_roll].enabled)

  draw_sweep_indicator(1, 25, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    square1_selected_light, square1_selected_dark, -- icon color when highlighted
    shadow_apu[0x4001])

  draw_volume_envelope_indicator(1, 34, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    square1_selected_light, square1_selected_dark, -- icon color when highlighted
    shadow_apu[0x4000])

  draw_volume_bar(1, 43, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    square1_selected_light, square1_selected_dark, -- icon color when highlighted
    shadow_apu[0x4000],
    square1_roll[#square1_roll].volume)

  local square2_duty = (shadow_apu[0x4004] & 0xC0) >> 6
  local square2_selected_light = SQUARE2_COLORS[square2_duty + 1]
  local square2_selected_dark = square2_selected_light + 0xB0000000

  tiny_string(1, 60, "Pulse 2", 0xFFFFFF)
  draw_raw_registers(1, 66, BOX_OUTLINE_COLOR, SHADOW_COLOR, UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR,
    {shadow_apu[0x4004],shadow_apu[0x4005],shadow_apu[0x4006],shadow_apu[0x4007]})
  draw_duty_indicator(1, 75, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    square2_selected_light, square2_selected_dark, -- icon color when highlighted
    (shadow_apu[0x4004] & 0xC0) >> 6, square2_roll[#square2_roll].enabled)

  draw_sweep_indicator(1, 84, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    square2_selected_light, square2_selected_dark, -- icon color when highlighted
    shadow_apu[0x4005])

  draw_volume_envelope_indicator(1, 93, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    square2_selected_light, square2_selected_dark, -- icon color when highlighted
    shadow_apu[0x4004])

  draw_volume_bar(1, 102, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    square2_selected_light, square2_selected_dark, -- icon color when highlighted
    shadow_apu[0x4004],
    square2_roll[#square2_roll].volume)

  local triangle_selected_light = TRIANGLE_COLORS[1]
  local triangle_selected_dark = triangle_selected_light + 0xB0000000

  tiny_string(1, 119, "Triangle", 0xFFFFFF)

  draw_raw_registers(1, 126, BOX_OUTLINE_COLOR, SHADOW_COLOR, UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR,
    {shadow_apu[0x4008],nil,shadow_apu[0x400A],shadow_apu[0x400B]})

  draw_triangle_indicator(1, 135, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    triangle_selected_light, triangle_selected_dark, -- icon color when highlighted
    triangle_roll[#triangle_roll].enabled)

  local noise_mode = ((shadow_apu[0x400E] & 0x80) >> 7)
  local noise_selected_light = NOISE_COLORS[noise_mode+1]
  local noise_selected_dark = noise_selected_light + 0xB0000000

  tiny_string(1, 195, "Noise", 0xFFFFFF)

  draw_raw_registers(1, 202, BOX_OUTLINE_COLOR, SHADOW_COLOR, UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR,
    {shadow_apu[0x400C],nil,shadow_apu[0x400E],shadow_apu[0x400F]})
  draw_noise_mode(1, 211, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    noise_selected_light, noise_selected_dark, -- icon color when highlighted
    shadow_apu[0x400E], noise_roll[#noise_roll].enabled)


  draw_volume_envelope_indicator(1, 220, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    noise_selected_light, noise_selected_dark, -- icon color when highlighted
    shadow_apu[0x400C])

  draw_volume_bar(1, 229, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    noise_selected_light, noise_selected_dark, -- icon color when highlighted
    shadow_apu[0x400C],
    noise_roll[#noise_roll].volume)

  local dpcm_selected_light = DMC_PLAYING_COLOR
  local dpcm_selected_dark = dpcm_selected_light + 0xB0000000

  tiny_string(1, 159, "DPCM", 0xFFFFFF)

  draw_raw_registers(1, 166, BOX_OUTLINE_COLOR, SHADOW_COLOR, UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR,
    {shadow_apu[0x4010],shadow_apu[0x4011],shadow_apu[0x4012],shadow_apu[0x4013]})

  draw_dpcm_sample_indicator(1, 175, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    dpcm_selected_light, dpcm_selected_dark, -- icon color when highlighted
    shadow_apu[0x4010], shadow_apu[0x4012], dmc_roll[#dmc_roll].playing)

  draw_dpcm_length_indicator(1, 184, 
    ICON_COLOR, --line color
    BOX_OUTLINE_COLOR, SHADOW_COLOR, -- box outline and shadow
    UNSELECTED_LIGHT_COLOR, UNSELECTED_DARK_COLOR, -- icon color when darkened
    dpcm_selected_light, dpcm_selected_dark, -- icon color when highlighted
    shadow_apu[0x4010], shadow_apu[0x4013], dmc_roll[#dmc_roll].playing)
end

function mesen_draw()
  local state = emu.getState()
  local clockRate = state["clockRate"]
  --             x   y   str            fore: argb  back: argb 
  --emu.drawString(10, 10, "Hello Mesen",   0x80ff0000, 0x80FFFFFF)
  --emu.drawString(10, 20, "P1: "..apu.square1.period,   0xff0000, 0xFFFFFF)

  local apu = {
    square1 = {
      frequency = clockRate / 16 / (state["apu.square1.realPeriod"] + 1),
      envelope = { volume = state["apu.square1.envelope.volume"] },
      lengthCounter = { counter = state["apu.square1.envelope.lengthCounter.counter"] },
      duty = state["apu.square1.duty"]
     },
     square2 = {
      frequency = clockRate / 16 / (state["apu.square2.realPeriod"] + 1),
      envelope = { volume = state["apu.square2.envelope.volume"] },
      lengthCounter = { counter = state["apu.square2.envelope.lengthCounter.counter"] },
      duty = state["apu.square2.duty"]
     },
     triangle = {
       frequency = clockRate / 32 / (state["apu.triangle.timer.period"] + 1),
       lengthCounter = { counter = state["apu.triangle.lengthCounter.counter"] },
       period = state["apu.triangle.timer.period"],
       enabled = state["apu.triangle.lengthCounter.enabled"]
     },
     noise = {
       period = state["apu.noise.timer.period"],
       mode = state["apu.noise.modeFlag"],
       envelope = { 
         constantVolume = state["apu.noise.envelope.constantVolume"],
         volume = state["apu.noise.envelope.volume"],
         counter = state["apu.noise.envelope.counter"]
       },
       lengthCounter = { counter =  state["apu.noise.envelope.lengthCounter.counter"] }
     },
     dmc = {
       bytesRemaining = state["apu.dmc.bytesRemaining"],
       outputVolume = state["apu.dmc.timer.lastOutput"],
       period = state["apu.dmc.timer.period"],
       sampleAddr = state["apu.dmc.sampleAddr"],
     }
  }

  update_piano_roll(apu.square1, square1_roll)
  update_piano_roll(apu.square2, square2_roll)
  update_piano_roll(apu.triangle, triangle_roll)
  update_noise_roll(apu.noise, noise_roll)
  update_dmc_roll(apu.dmc, dmc_roll)

  draw_piano_background()
  draw_piano_strings()
  draw_piano_keys()
  draw_noise_strings()
  draw_noise_pads(noise_roll[#noise_roll], NOISE_COLORS)
  
  draw_piano_roll(emu, square1_roll, SQUARE1_COLORS) 
  draw_piano_roll(emu, square2_roll, SQUARE2_COLORS) 
  draw_piano_roll(emu, triangle_roll, TRIANGLE_COLORS)
  draw_noise_roll(emu, noise_roll, NOISE_COLORS)
  draw_dmc_roll(emu, dmc_roll)
  draw_key_spot(square1_roll[#square1_roll], SQUARE1_COLORS)
  draw_key_spot(square2_roll[#square2_roll], SQUARE2_COLORS)
  draw_key_spot(triangle_roll[#square1_roll], TRIANGLE_COLORS)
  draw_dmc_head(dmc_roll[#dmc_roll])

  draw_apu_registers()

  handle_input()
end

emu.addEventCallback(mesen_draw, emu.eventType.endFrame);