// SPDX-License-Identifier: Unlicense

#include "highway2.h"

#ifdef SHR3D_RENDERER_DEVELOPMENT

#include "data.h"
#include "geometry.h"
#include "global.h"
#include "opengl.h"
#include "shader.h"

#include <malloc.h>

static i32 getNoteBeginIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()); ++i)
  {
    const Song::Note& note = Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i];
    const TimeNS noteTime = -note.timeNS - note.sustainNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return i;
  }

  return i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()) - 1;
}

static i32 getNoteEndIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()) - 1; i >= 0; --i)
  {
    const Song::Note& note = Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i];
    const TimeNS noteTime = -note.timeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

static i32 getChordBeginIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()); ++i)
  {
    const Song::Chord& chord = Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[i];
    const TimeNS noteTime = -chord.timeNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return min_(0, i - 1);
  }

  return i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()) - 1;
}

static i32 getChordEndIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()) - 1; i >= 0; --i)
  {
    const Song::Chord& chord = Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[i];
    const TimeNS noteTime = -chord.timeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

static i32 getSustainBeginIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()); ++i)
  {
    const Song::Sustain& sustain = Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains[i];
    const TimeNS noteTime = -sustain.endTimeNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return i;
  }

  return i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()) - 1;
}

static i32 getSustainEndIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()) - 1; i >= 0; --i)
  {
    const Song::Sustain& sustain = Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains[i];
    const TimeNS noteTime = -sustain.startTimeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

static i32 getBeatBeginIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTracks[selectedArrangementIndex].beats.size()); ++i)
  {
    const Song::Beat& beat = Global::songTracks[selectedArrangementIndex].beats[i];
    const TimeNS noteTime = -beat.timeNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return i;
  }

  return i32(Global::songTracks[selectedArrangementIndex].beats.size()) - 1;
}

static i32 getBeatEndIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTracks[selectedArrangementIndex].beats.size()) - 1; i >= 0; --i)
  {
    const Song::Beat& beat = Global::songTracks[selectedArrangementIndex].beats[i];
    const TimeNS noteTime = -beat.timeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

static i32 getArpeggioBeginIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()); ++i)
  {
    const Song::Arpeggio& arpeggios = Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios[i];
    const TimeNS noteTime = -arpeggios.endTimeNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return i;
  }

  return i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()) - 1;
}

static i32 getArpeggioEndIndex2(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()) - 1; i >= 0; --i)
  {
    const Song::Arpeggio& arpeggios = Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios[i];
    const TimeNS noteTime = -arpeggios.startTimeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

// struct contains only indecies that are usefull
struct TrackIndex2
{
  i32 currentAnchor = -1;
  i32 noteBegin = -1;
  i32 noteEnd = -1;
  i32 chordBegin = -1;
  i32 chordEnd = -1;
  i32 sustainBegin = -1;
  i32 sustainEnd = -1;
  i32 beatBegin = -1;
  i32 beatEnd = -1;
  i32 arpeggioBegin = -1;
  i32 arpeggioEnd = -1;
};

static TrackIndex2 getTrackIndex2(const ArrangementIndex selectedArrangementIndex)
{
  TrackIndex2 trackIndex
  {
    .currentAnchor = Song::getCurrentAnchorIndex(selectedArrangementIndex),
    .noteBegin = getNoteBeginIndex2(selectedArrangementIndex),
    .noteEnd = getNoteEndIndex2(selectedArrangementIndex),
    .chordBegin = getChordBeginIndex2(selectedArrangementIndex),
    .chordEnd = getChordEndIndex2(selectedArrangementIndex),
    .sustainBegin = getSustainBeginIndex2(selectedArrangementIndex),
    .sustainEnd = getSustainEndIndex2(selectedArrangementIndex),
    .beatBegin = getBeatBeginIndex2(selectedArrangementIndex),
    .beatEnd = getBeatEndIndex2(selectedArrangementIndex),
    .arpeggioBegin = getArpeggioBeginIndex2(selectedArrangementIndex),
    .arpeggioEnd = getArpeggioEndIndex2(selectedArrangementIndex)
  };

  ASSERT(trackIndex.currentAnchor >= 0);
  ASSERT(trackIndex.currentAnchor < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()));
  ASSERT(trackIndex.noteBegin >= -1);
  ASSERT(trackIndex.noteBegin < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()));
  ASSERT(trackIndex.noteEnd >= -2);
  ASSERT(trackIndex.noteEnd < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()));
  ASSERT(trackIndex.chordBegin >= -1);
  ASSERT(trackIndex.chordBegin < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()));
  ASSERT(trackIndex.chordEnd >= -2);
  ASSERT(trackIndex.chordEnd < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()));
  ASSERT(trackIndex.sustainBegin >= -1);
  ASSERT(trackIndex.sustainBegin < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()));
  ASSERT(trackIndex.sustainEnd >= -2);
  ASSERT(trackIndex.sustainEnd < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()));
  ASSERT(trackIndex.beatBegin >= -1);
  ASSERT(trackIndex.beatBegin < i32(Global::songTracks[selectedArrangementIndex].beats.size()));
  ASSERT(trackIndex.beatEnd >= -2);
  ASSERT(trackIndex.beatEnd < i32(Global::songTracks[selectedArrangementIndex].beats.size()));
  ASSERT(trackIndex.arpeggioBegin >= -1);
  ASSERT(trackIndex.arpeggioBegin < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()));
  ASSERT(trackIndex.arpeggioEnd >= -2);
  ASSERT(trackIndex.arpeggioEnd < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()));

  return trackIndex;
}

static void setStringColor2(const Highway::Ctx& ctx, GLint colorUniform, const i32 string, const f32 alpha = -1.0f)
{
  ASSERT(string >= 0);
  ASSERT(string <= Const::highwayInstrumentGuitarStringCount - 1);

  const vec4& colorVec = ctx.instrumentStringColors[string];
  GL(glUniform4f(colorUniform, colorVec.r, colorVec.g, colorVec.b, alpha == -1.0f ? colorVec.a : alpha));
}

static i32 reverseStringFix2(const Highway::Ctx& ctx, i32 i)
{
  if (!Settings::highwayReverseStrings)
    return i;

  return ctx.instrumentStringCount - 1 - i;
}

static TimeNS getAnchorTimeEndNS2(const ArrangementIndex selectedArrangementIndex, i32 anchorIndex)
{
  if (anchorIndex == Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size() - 1)
  {
    return Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd;
  }

  const Song::Anchor& anchor1 = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[anchorIndex + 1];
  return anchor1.timeNS;
}

static void drawGroundAnchor2(const Highway::Ctx& ctx, const mat4& viewProjectionMat, const Song::Anchor& anchor, const TimeNS noteTimeBegin, const TimeNS noteTimeEnd)
{
  GL(glUseProgram(Shader::groundAnchor));

  GL(glUniform1f(Shader::groundAnchorUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::groundAnchorUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  GL(glUniform1f(Shader::groundAnchorUniformFadeNearDistance, Settings::highwayFadeNearDistance));
  GL(glUniform1f(Shader::groundAnchorUniformFadeNearStrength, Settings::highwayFadeNearStrength));

  const f32 front = min_(TimeNS_To_Seconds(noteTimeBegin) * Settings::highwayScrollSpeed, 0.0f);
  const f32 back = TimeNS_To_Seconds(noteTimeEnd) * Settings::highwayScrollSpeed;

  for (i32 i = 0; i < anchor.width; ++i)
  {
    const f32 left = ctx.instrumentFretPosition[anchor.fret - 1 + i];
    const f32 right = ctx.instrumentFretPosition[anchor.fret + i];

    const GLfloat v[] = {
      left , -0.36f, front, 0.0f, 1.0f,
      right, -0.36f, front, 1.0f, 1.0f,
      left, -0.36f, back, 0.0f, 0.0f,
      right, -0.36f, back, 1.0f, 0.0f,
    };

    mat4 model;
    GL(glUniformMatrix4fv(Shader::groundAnchorUniformModel, 1, GL_FALSE, &model.m00));

    GL(glUniformMatrix4fv(Shader::groundAnchorUniformViewProjection, 1, GL_FALSE, &viewProjectionMat.m00));

    if (Const::hasFretDotInlay[anchor.fret + i])
      GL(glUniform4f(Shader::groundAnchorUniformColor, Settings::highwayAnchorColor[0].r, Settings::highwayAnchorColor[0].g, Settings::highwayAnchorColor[0].b, Settings::highwayAnchorColor[0].a));
    else
        GL(glUniform4f(Shader::groundAnchorUniformColor, Settings::highwayAnchorColor[1].r, Settings::highwayAnchorColor[1].g, Settings::highwayAnchorColor[1].b, Settings::highwayAnchorColor[1].a));

    GL(glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW));
    GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, sizeof(v) / (sizeof(f32) * 5)));
  }
}

static void drawGroundAnchors2(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& viewProjectionMat, i32 currentAnchorIndex)
{
  for (i32 i = currentAnchorIndex; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++i)
  {
    const Song::Anchor& anchor0 = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[i];
    const TimeNS noteTimeBegin = -anchor0.timeNS + Global::musicTimeElapsedNS;

    if (noteTimeBegin < timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      break;

    const TimeNS noteTimeEnd = -getAnchorTimeEndNS2(selectedArrangementIndex, i) + Global::musicTimeElapsedNS;

    drawGroundAnchor2(ctx, viewProjectionMat, anchor0, noteTimeBegin, noteTimeEnd);
  }
}

static void drawNotes2(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& viewProjectionMat, f32 /*fretboardNearestNoteDistance*/[Const::highwayInstrumentGuitarStringCount][25], i32 currentAnchorIndex, i32 noteBeginIndex, i32 noteEndIndex, i32 /*sustainBeginIndex*/, i32 /*sustainEndIndex*/)
{
  if (noteEndIndex <= 0)
    return;

  struct InstanceData
  {
    mat4 mvp;
    vec4 color;
  };

  InstanceData* instanceData = reinterpret_cast<InstanceData*>(alloca(sizeof(InstanceData) * (noteEndIndex - noteBeginIndex + 1))); // if this crashes, the stack size must be increased
  i32 instanceCount = 0;

  for (i32 i = noteEndIndex; i >= noteBeginIndex; --i)
  {
    const Song::Note& note = Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i];

    const TimeNS noteTime = -note.timeNS + Global::musicTimeElapsedNS;

    if (noteTime - note.sustainNS > 0)
      continue;
    if (noteTime < timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      continue;

    i32 noteAnchorIndex = currentAnchorIndex;
    for (i32 j = currentAnchorIndex; j < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++j)
    {
      const Song::Anchor& anchor0 = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[j];
      const TimeNS anchorTimeEnd = getAnchorTimeEndNS2(selectedArrangementIndex, j);

      if (note.timeNS >= anchor0.timeNS && note.timeNS < anchorTimeEnd)
      {
        noteAnchorIndex = j;
        break;
      }
    }

    if (noteTime < 0)
    {
      if (note.fret != 0)
      {
        const f32 x = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);

        mat4 model;
        if (Settings::highwayNoteRotate != 0 && note.rotation)
        {
          const f32 rotZ = clamp((-TimeNS_To_Seconds(noteTime) - Settings::highwayNoteRotateEndTime) * Settings::highwayNoteRotateSpeed, 0.0f, 0.5f * PI_) * f32(Settings::highwayNoteRotate);
          model.m00 = Settings::highwayNoteWidth * cos(rotZ);
          model.m01 = -sin(rotZ);
          model.m10 = sin(rotZ);
          model.m11 = Settings::highwayNoteWidth * cos(rotZ);
        }
        else
        {
          model.m00 = Settings::highwayNoteWidth;
          model.m11 = Settings::highwayNoteHeight;
        }
        model.m03 = x;

        f32 bendSteps = 0.0f;
        if (note.bendValues.size() >= 1)
        {
          if (note.timeNS == note.bendValues[0].timeNS)
          {
            bendSteps = note.bendValues[0].step * (1.0f - clamp((-TimeNS_To_Seconds(noteTime) - Settings::highwayNoteBendEndTime) * Settings::highwayNoteBendSpeed, 0.0f, 1.0f));
            bendSteps = note.string < 3 ? bendSteps : -bendSteps;
          }
        }

        model.m13 = (f32(reverseStringFix2(ctx, note.string + ctx.instrumentStringOffset)) + bendSteps) * *ctx.instrumentStringSpacing;
        model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

        instanceData[instanceCount].mvp = vec::multiply(viewProjectionMat, model);
        instanceData[instanceCount].color = ctx.instrumentStringColors[note.string];
        instanceData[instanceCount].color.a = 1.0f;
        ++instanceCount;
      }
    }
  }

  {
    GL(glUseProgram(Shader::highwayFadeFarInstanced));
    GL(glUniform1f(Shader::highwayFadeFarInstancedUniformViewDistance, Settings::highwayViewDistance));
    GL(glUniform1f(Shader::highwayFadeFarInstancedUniformFadeFarDistance, Settings::highwayFadeFarDistance));

    GLuint instanceVBO;
    GL(glGenBuffers(1, &instanceVBO));
    GL(glBindBuffer(GL_ARRAY_BUFFER, instanceVBO));
    GL(glBufferData(GL_ARRAY_BUFFER, instanceCount * 20 * sizeof(float), &instanceData[0], GL_STATIC_DRAW));

    GL(glEnableVertexAttribArray(2));
    GL(glEnableVertexAttribArray(3));
    GL(glEnableVertexAttribArray(4));
    GL(glEnableVertexAttribArray(5));
    GL(glEnableVertexAttribArray(6));
    GL(glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), nullptr));
    GL(glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), (void*)(4 * sizeof(GLfloat))));
    GL(glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), (void*)(8 * sizeof(GLfloat))));
    GL(glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), (void*)(12 * sizeof(GLfloat))));
    GL(glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), (void*)(16 * sizeof(GLfloat))));
    GL(glVertexAttribDivisor(2, 1));
    GL(glVertexAttribDivisor(3, 1));
    GL(glVertexAttribDivisor(4, 1));
    GL(glVertexAttribDivisor(5, 1));
    GL(glVertexAttribDivisor(6, 1));
    GL(glDrawElementsInstanced_(noteRect, instanceCount));
    GL(glVertexAttribDivisor(2, 0));
    GL(glVertexAttribDivisor(3, 0));
    GL(glVertexAttribDivisor(4, 0));
    GL(glVertexAttribDivisor(5, 0));
    GL(glVertexAttribDivisor(6, 0));
    GL(glDisableVertexAttribArray(2));
    GL(glDisableVertexAttribArray(3));
    GL(glDisableVertexAttribArray(4));
    GL(glDisableVertexAttribArray(5));
    GL(glDisableVertexAttribArray(6));

    GL(glDeleteBuffers(1, &instanceVBO));

    GL(glBindBuffer(GL_ARRAY_BUFFER, Global::vbo));
  }
}

static f32 stringFadeUnplayedAlpha2(const Highway::Ctx& ctx, f32 fretboardNearestNoteDistance[25], i32 j)
{
  f32 alpha = Const::highwayStringFadeUnplayedAlpha;

  f32 distance = F32::inf;
  for (i32 i = 0; i < 25; ++i)
  {
    if (fretboardNearestNoteDistance[i] == 0.0f)
      continue;

    distance = min_(distance, fretboardNearestNoteDistance[i]);
  }

  if (distance < Const::highwayStringFadeUnplayedNearDistance)
  {
    ctx.highwayStringFadeUnplayedDecayTime[j] = Const::highwayStringFadeUnplayedDecayTime;
    alpha = 1.0f;
  }
  else if (distance < Const::highwayStringFadeUnplayedFarDistance)
    alpha = map_(distance, Const::highwayStringFadeUnplayedNearDistance, Const::highwayStringFadeUnplayedFarDistance, 1.0f, Const::highwayStringFadeUnplayedAlpha);

  if (ctx.highwayStringFadeUnplayedDecayTime[j] > 0.0f)
  {
    ctx.highwayStringFadeUnplayedDecayTime[j] -= TimeNS_To_Seconds(Global::frameDelta);
    if (ctx.highwayStringFadeUnplayedDecayTime[j] < 0.0f)
      ctx.highwayStringFadeUnplayedDecayTime[j] = 0.0f;
    else
      alpha = max_(alpha, (1.0f - Const::highwayStringFadeUnplayedAlpha) * (ctx.highwayStringFadeUnplayedDecayTime[j] / Const::highwayStringFadeUnplayedDecayTime) + Const::highwayStringFadeUnplayedAlpha);
  }

  ASSERT(alpha >= Const::highwayStringFadeUnplayedAlpha);
  ASSERT(alpha <= 1.0f);

  return alpha;
}

static void drawString2(const Highway::Ctx& ctx, const mat4& viewProjectionMat, i32 i, GLint modelViewProjectionUniform, GLint colorUniform, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25])
{
  const f32 alpha = Settings::highwayStringFadeUnplayed
    ? stringFadeUnplayedAlpha2(ctx, fretboardNearestNoteDistance[i + ctx.instrumentStringOffset], i)
    : 1.0f;

  setStringColor2(ctx, colorUniform, i + ctx.instrumentHideFirstStrings, alpha);

  mat4 model;
  model.m11 = 1.0f + (i + ctx.instrumentHideFirstStrings) * Const::highwayStringGaugeMultiplier;
  model.m22 = 1.0f + (i + ctx.instrumentHideFirstStrings) * Const::highwayStringGaugeMultiplier;
  model.m13 = f32(reverseStringFix2(ctx, i)) * *ctx.instrumentStringSpacing;
  const mat4 mvp = vec::multiply(viewProjectionMat, model);
  GL(glUniformMatrix4fv(modelViewProjectionUniform, 1, GL_FALSE, &mvp.m00));

  GL(glDrawElements_(string));
}

static void drawFretboardStrings2(const Highway::Ctx& ctx, const mat4& viewProjectionMat, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25])
{
  // draw unwond wound
  const i32 noUnwoundStrings = max_(0, *ctx.instrumentFirstWoundString - ctx.instrumentHideFirstStrings);

  if (noUnwoundStrings >= 1)
  {
    GL(glUseProgram(Shader::string));

    for (i32 i = 0; i < noUnwoundStrings; ++i)
    {
      drawString2(ctx, viewProjectionMat, i, Shader::stringUniformModelViewProjection, Shader::stringUniformColor, fretboardNearestNoteDistance);
    }
  }

  // draw string wound
  if (noUnwoundStrings < ctx.instrumentStringCount)
  {
    GL(glUseProgram(Shader::stringWound));

    for (i32 i = noUnwoundStrings; i < ctx.instrumentStringCount; ++i)
    {
      drawString2(ctx, viewProjectionMat, i, Shader::stringWoundUniformModelViewProjection, Shader::stringWoundUniformColor, fretboardNearestNoteDistance);
    }
  }
}

static void drawFretboardFrets2(const Highway::Ctx& ctx, const mat4& viewProjectionMat)
{
  GL(glUseProgram(Shader::fret));

  mat4 model;
  model.m11 = 0.0279f + 0.2948f * f32(ctx.instrumentStringCount) * *ctx.instrumentStringSpacing;
  model.m13 = -0.3f;
  const mat4 mvp = vec::multiply(viewProjectionMat, model);
  GL(glUniformMatrix4fv(Shader::fretUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

  GL(glUniform1fv(Shader::fretUniformFretPositionOffsetX, ARRAY_SIZE(ctx.instrumentFretPosition), ctx.instrumentFretPosition));
  GL(glUniform4f(Shader::fretUniformColor, Settings::highwayFretboardFretColor.r, Settings::highwayFretboardFretColor.g, Settings::highwayFretboardFretColor.b, Settings::highwayFretboardFretColor.a));

  GL(glDrawElementsInstanced_(fret, ARRAY_SIZE(ctx.instrumentFretPosition)));
}

void Highway2::render(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& viewProjectionMat)
{
  if (Global::selectedSongIndex == -1)
    return;
  if (ctx.instrumentStringColors == nullptr)
    return;

  f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25] = { };

  const TrackIndex2 TrackIndex2 = getTrackIndex2(selectedArrangementIndex);

  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));

  drawGroundAnchors2(ctx, selectedArrangementIndex, viewProjectionMat, TrackIndex2.currentAnchor);

  drawNotes2(ctx, selectedArrangementIndex, viewProjectionMat, fretboardNearestNoteDistance, TrackIndex2.currentAnchor, TrackIndex2.noteBegin, TrackIndex2.noteEnd, TrackIndex2.sustainBegin, TrackIndex2.sustainEnd);

  GL(glBindVertexArray(Global::staticDrawVao));
  drawFretboardStrings2(ctx, viewProjectionMat, fretboardNearestNoteDistance);
  drawFretboardFrets2(ctx, viewProjectionMat);
  GL(glBindVertexArray(Global::dynamicDrawVao));
}

#endif // SHR3D_RENDERER_DEVELOPMENT
