/*
 * (C) azslow.com 2024
 *
 * There are several limitations and assumptions.
 * Project tempo must be constant.
 * Only tracks with "compatible loop" are assumed to be looped tracks.
 * The only exception is current track during this command execution, if it has
 * a single item started at the beginning, the item is converted into "compatible loop".
 * "Compatible loop" means there is just one item on the track, it starts at the beginning,
 * has looped source and looped length is dividable by 2.
 *
 * It is assumed loop recording is stopped during the first half of the measure after
 * its desired length. For other logic, DesiredLoopLength() function should be modified.
 *
 * Note the script set "Options: Loop recording always adds takes",
 * controls "Repeat" and looping range.
 *
 * Known problems:
 *   MIDI notes which cross loop border are repeated. 
 */

// We not always modify the project, no reason to always make Undo block
in_undo = 0;
function StartUndo() (
  !in_undo ? (
    Undo_BeginBlock();
    in_undo = 1;
  );
);
function EndUndo(desc flags) (
  in_undo ? (
    Undo_EndBlock(desc, flags);
    in_undo = 0;
  );
);

// Distinguish between looped tracks and other tracks.
// Track with loop:
//   1) has just one item
//   2) the item starts at the beggining of the project
//   3*) the item has "B_LOOPSRC" flag set
// The condition (3) is not used for "current" track,
// the item will be "prepared" for looping by this script
//
// Returns loop MediaItem or 0

function LoopTrackItem(tr current) (
 tr ? (
   CountTrackMediaItems(tr) == 1 ? (
    it = GetTrackMediaItem(tr, 0);
    pos = GetMediaItemInfo_Value(it, "D_POSITION");
    pos < 0.00001 ? (
      current || GetMediaItemInfo_Value(it, "B_LOOPSRC") ? it : 0;
    ) : 0;
   ) : 0;
 ) : 0;
);

// return current looped length in seconds
function GetSourceLength(it) local(tk pr src slen inQN) (
  it && GetMediaItemNumTakes(it) ? (
    tk = GetMediaItemTake(it, 0);
    pr = GetMediaItemTakeInfo_Value(tk, "D_PLAYRATE");
    src = GetMediaItemTake_Source(tk);
    inQN = 0; // not working correctly otherwise... bug???
    slen = GetMediaSourceLength(src, inQN) / pr;
    inQN ? (
      slen = TimeMap_QNToTime(slen);
    ); // else length is already in seconds
    slen;
  ) : 0
);

// unroll item
// returns (potentially replacement) item
function Unroll(it) local(i tr itn lit slen) (
  it ? (
    GetMediaItemNumTakes(it) > 1 ? (
      tr = GetMediaItem_Track(it);
      StartUndo();
      SelectAllMediaItems(0, 0);
      SetMediaItemSelected(it, 1);
      Main_OnCommand(40643, 0); // Take: Explode takes of items in order
      // the last take is incomplete, but will have the length of the whole loop
      // so truncate last take to actual length
      itn = GetTrackNumMediaItems(tr);
      itn ? (
        lit = GetTrackMediaItem(tr, itn - 1);
        slen = GetSourceLength(lit);
        SetMediaItemLength(lit, slen, 0);
      );
      
      i = 0; // select all items on the track
      loop(CountTrackMediaItems(tr),
        SetMediaItemSelected(GetTrackMediaItem(tr, i), 1);
        i += 1;
      );
      Main_OnCommand(40362, 0); // Item: Glue items, ignoring time selection
      it = GetTrackMediaItem(tr, 0); // it is new item
    ) : it;
  ) : it;
);


// return current looped length in measures
function GetLoopLength(it) local(tk pr src sqn inQN m mstart mend offs len rev) (
  it && GetMediaItemNumTakes(it) == 1 ? (
    tk = GetMediaItemTake(it, 0);
    pr = GetMediaItemTakeInfo_Value(tk, "D_PLAYRATE");
    src = GetMediaItemTake_Source(tk);
    inQN = 0; // not working correctly otherwise... bug???
    sqn = GetMediaSourceLength(src, inQN) / pr;
    !inQN ? (
      // length in seconds, may already have section
      PCM_Source_GetSectionInfo(src, offs, len, rev) ? (
        sqn = len;
      );
      sqn = TimeMap_timeToQN(sqn); // loops always start at the beginning
    ); // else length is already in QN
    m = TimeMap_QNToMeasures(0, sqn, mstart, mend); // loops always start at the beginning
    // we want fractional part and measures counted from 0
    sqn < mend ? (m - 1 + (sqn - mstart) / (mend - mstart);) : m
  ) : 0
);

// from specifined length in measures (zero based, with fraction)
// calculates desired length.
function DesiredLoopLength(m) local(fm dm) (
  m ? (
    fm = m - floor(m);
    fm > 0.000001 && fm < 0.5 ? (
      m = floor(m);
    );
    dm = 1; // desired length in measures
    while ( dm < m ) (
      dm *= 2;
    );
    dm;
  ) : 0;
);

// correct loop length, if required
// set source looping, if not yet set
function CorrectLoopLength(it) (
  m = GetLoopLength(it);
  dm = DesiredLoopLength(m);
  abs(m - dm) > 0.000001 ? (
    TimeMap_GetMeasureInfo(0, dm, qn, qne);
    tk = GetMediaItemTake(it, 0);
    qn *= GetMediaItemTakeInfo_Value(tk, "D_PLAYRATE"); 
    sec = TimeMap_QNToTime(qn);
    StartUndo();
    // For MIDI, unlike in GUI, changing length with loop off doesn't work
    // For Audio, there is no PCM_Source set length...
    // so change to desired loop length, glue, set loop.
    SetMediaItemInfo_Value(it, "B_LOOPSRC", 0);
    SetMediaItemInfo_Value(it, "D_LENGTH", sec);
    SelectAllMediaItems(0, 0);
    SetMediaItemSelected(item, 1);
    Main_OnCommand(40362, 0); // Item: Glue items, ignoring time selection
    SetMediaItemInfo_Value(it, "B_LOOPSRC", 1);
  ) : (
    !GetMediaItemInfo_Value(it, "B_LOOPSRC") ? (
      StartUndo();
      SetMediaItemInfo_Value(it, "B_LOOPSRC", 1); // set looping even when clip is find
    )
  );
);

function MeasureToTime(m) local(qn) (
  TimeMap_GetMeasureInfo(0, m, qn);
  TimeMap_QNToTime(qn);
);

// prepare item on current track (if that can be loop track)
trk = GetLastTouchedTrack();
item = LoopTrackItem(trk, 1);
item = Unroll(item);
CorrectLoopLength(item);

// set loops length to cover whole project, but at "loopable" position
i = 0; // tracks counter
// tl[] - track loop length (or zero)
mtl = 0; // max track loop length (project wide looping divider)
prl = 0; // project length (the end of the last item in the project, except for loops)
loop (GetNumTracks(), (
  tr = GetTrack(0, i);
  it = LoopTrackItem(tr, 0);
  it ? (
    m = GetLoopLength(it);
    dm = DesiredLoopLength(m);
    m && abs(m - dm) < 0.000001 ? ( // "compatible track"
      tl[i] = dm;
    ) : ( // "incompatible track"
      tl[i] = 0;
    );
  ) : (
    tl[i] = 0;
    itn = GetTrackNumMediaItems(tr);
    itn ? (
      it = GetTrackMediaItem(tr, itn - 1);
      ite = GetMediaItemInfo_Value(it, "D_POSITION") +  GetMediaItemInfo_Value(it, "D_LENGTH");
      ite > prl ? prl = ite;
    );
  );
  tl[i] > mtl ? mtl = tl[i];
  /*
  sprintf(#msg, "Track[%u] loop length: %g %g\n", i, tl[i], m); 
  ShowConsoleMsg(#msg);
  //*/
  i += 1;
) );
mtl ? ( // we don't have loops to correct otherwise
  m = TimeMap_QNToMeasures(0, TimeMap_timeToQN(prl));
  mpe = floor(m / mtl) * mtl; // project end in measures, starting point
  while(MeasureToTime(mpe) < prl) (
    mpe += mtl;
  );
  mpe < mtl ? mpe = mtl; // in case we have loops only
  pe = MeasureToTime(mpe); // time == length all loops should be (in seconds)
  // update loops
  i = 0;
  loop (GetNumTracks(), (
    tl[i] ? ( // update loop tracks only
      tr = GetTrack(0, i);
      it = LoopTrackItem(tr, 0);
      it ? (
        il = GetMediaItemInfo_Value(it, "D_LENGTH");
        abs(il - pe) > 0.000001 ? (
          StartUndo();
          SetMediaItemLength(it, pe, 0);
        );
      );
    );
    i += 1;
  ) );
  // Set loop region
  GetSet_LoopTimeRange(1, 1, 0, pe, 1);
  GetSetRepeat(1);
) : (
  // no loops yet, disable repeat
  GetSetRepeat(0);
);

SetEditCurPos(0, 1, 0); // rewind

!GetToggleCommandState(40114) ? Main_OnCommand(40114, 0); // Options: Loop recording always adds takes

EndUndo("Prepare looping", 0);

