text: const constants = require("../constants");
  const getShowData = require("./get-show-data")();
  const random = require('../helperFuncs').random;
  const throttle = require('./throttle');
  
  
  const MINUTE = 60*1000;
  const DAY = 24*60*MINUTE;
  const LIMIT = 40000;
  
  
  
  function getShow(program) {
  
      let d = getShowData(program);
      if (! d.hasShow) {
          return null;
      } else {
          d.description = d.showDisplayName;
          d.id = d.showId;
          return d;
      }
  }
  
  
  function shuffle(array, lo, hi ) {
      if (typeof(lo) === 'undefined') {
          lo = 0;
          hi = array.length;
      }
      let currentIndex = hi, temporaryValue, randomIndex
      while (lo !== currentIndex) {
          randomIndex =  random.integer(lo, currentIndex-1);
          currentIndex -= 1
          temporaryValue = array[currentIndex]
          array[currentIndex] = array[randomIndex]
          array[randomIndex] = temporaryValue
      }
      return array
  }
  
  function _wait(t) {
      return new Promise((resolve) => {
        setTimeout(resolve, t);
      });
  }
  
  function getProgramId(program) {
      let s = program.serverKey;
      if (typeof(s) === 'undefined') {
          s = 'unknown';
      }
      let p = program.key;
      if (typeof(p) === 'undefined') {
          p = 'unknown';
      }
      return s + "|" + p;
  }
  
  function addProgramToShow(show, program) {
      if ( (show.id == 'flex.') || show.id.startsWith("redirect.")  ) {
          //nothing to do
          return;
      }
      let id = getProgramId(program)
      if(show.programs[id] !== true) {
          show.programs.push(program);
          show.programs[id] = true
      }
  }
  
  function getShowOrderer(show) {
      if (typeof(show.orderer) === 'undefined') {
  
          let sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
          sortedPrograms.sort((a, b) => {
              let showA = getShowData(a);
              let showB = getShowData(b);
              return showA.order - showB.order;
          });
  
          let position = 0;
          while (
              (position + 1 < sortedPrograms.length )
              &&
              (
                  getShowData(show.founder).order
                  !==
                  getShowData(sortedPrograms[position]).order
              )
          ) {
              position++;
          }
  
  
          show.orderer = {
  
              current : () => {
                  return sortedPrograms[position];
              },
  
              next: () => {
                  position = (position + 1) % sortedPrograms.length;
              },
  
          }
      }
      return show.orderer;
  }
  
  
  function getShowShuffler(show) {
      if (typeof(show.shuffler) === 'undefined') {
          if (typeof(show.programs) === 'undefined') {
              throw Error(show.id + " has no programs?")
          }
  
          let randomPrograms = JSON.parse( JSON.stringify(show.programs) );
          let n = randomPrograms.length;
          shuffle( randomPrograms, 0, n);
          let position = 0;
  
          show.shuffler  = {
  
              current : () => {
                  return randomPrograms[position];
              },
  
              next: () => {
                  position++;
                  if (position == n) {
                      let a = Math.floor(n / 2);
                      shuffle(randomPrograms, 0, a );
                      shuffle(randomPrograms, a, n );
                      position = 0;
                  }
              },
  
          }
      }
      return show.shuffler;
  }
  
  module.exports = async( programs, schedule  ) => {
      if (! Array.isArray(programs) ) {
          return { userError: 'Expected a programs array' };
      }
      if (typeof(schedule) === 'undefined') {
          return { userError: 'Expected a schedule' };
      }
      //verify that the schedule is in the correct format
      if (! Array.isArray(schedule.slots) ) {
          return { userError: 'Expected a "slots" array in schedule' };
      }
      if (typeof(schedule).period === 'undefined') {
          schedule.period = DAY;
      }
      for (let i = 0; i < schedule.slots.length; i++) {
          if (typeof(schedule.slots[i].duration) === 'undefined') {
              return { userError: "Each slot should have a duration" };
          }
          if (typeof(schedule.slots[i].showId) === 'undefined') {
              return { userError: "Each slot should have a showId" };
          }
          if (
              (schedule.slots[i].duration <= 0)
              || (Math.floor(schedule.slots[i].duration) != schedule.slots[i].duration)
          ) {
              return { userError: "Slot duration should be a integer number of milliseconds greater than 0" };
          }
          if ( isNaN(schedule.slots[i].cooldown)  ) {
              schedule.slots[i].cooldown = 0;
          }
          if ( isNaN(schedule.slots[i].weight)  ) {
              schedule.slots[i].weight = 1;
          }
      }
      if (typeof(schedule.pad) === 'undefined') {
          return { userError: "Expected schedule.pad" };
      }
      if (typeof(schedule.maxDays) == 'undefined') {
          return { userError: "schedule.maxDays must be defined." };
      }
      if (typeof(schedule.flexPreference) === 'undefined') {
          schedule.flexPreference = "distribute";
      }
      if (typeof(schedule.padStyle) === 'undefined') {
          schedule.padStyle = "slot";
      }
      if (schedule.padStyle !== "slot" && schedule.padStyle !== "episode") {
          return { userError: `Invalid schedule.padStyle value: "${schedule.padStyle}"` };
      }
      let flexBetween = ( schedule.flexPreference !== "end" );
  
      // throttle so that the stream is not affected negatively
      let steps = 0;
  
      let showsById = {};
      let shows = [];
  
      function getNextForSlot(slot, remaining) {
          //remaining doesn't restrict what next show is picked. It is only used
          //for shows with flexible length (flex and redirects)
          if (slot.showId === "flex.") {
              return {
                  isOffline: true,
                  duration: remaining,
              }
          }
          let show = shows[ showsById[slot.showId] ];
          if (slot.showId.startsWith("redirect.")) {
              return {
                  isOffline: true,
                  type: "redirect",
                  duration: remaining,
                  channel: show.channel,
              }
          } else if (slot.order === 'shuffle') {
              return getShowShuffler(show).current();
          } else if (slot.order === 'next') {
              return getShowOrderer(show).current();
          }
      }
      
      function advanceSlot(slot) {
          if ( (slot.showId === "flex.") || (slot.showId.startsWith("redirect") ) ) {
              return;
          }
          let show = shows[ showsById[slot.showId] ];
          if (slot.order === 'shuffle') {
              return getShowShuffler(show).next();
          } else if (slot.order === 'next') {
              return getShowOrderer(show).next();
          }
      }
  
      function makePadded(item) {
          let padOption = schedule.pad;
          if (schedule.padStyle === "slot") {
              padOption = 1;
          }
          let x = item.duration;
          let m = x % padOption;
          let f = 0;
          if ( (m > constants.SLACK) && (padOption - m > constants.SLACK) ) {
              f = padOption - m;
          }
          return {
              item: item,
              pad: f,
              totalDuration: item.duration + f,
          }
  
      }
  
      // load the programs
      for (let i = 0; i < programs.length; i++) {
          let p = programs[i];
          let show = getShow(p);
          if (show != null) {
              if (typeof(showsById[show.id] ) === 'undefined') {
                  showsById[show.id] = shows.length;
                  shows.push( show );
                  show.founder = p;
                  show.programs = [];
              } else {
                  show = shows[ showsById[show.id] ];
              }
              addProgramToShow( show, p );
          }
      }
  
      let s = schedule.slots;
      let ts = (new Date() ).getTime();
  
      let t0 = ts;
      let p = [];
      let t = t0;
  
      let hardLimit = t0 + schedule.maxDays * DAY;
  
      let pushFlex = (d) => {
          if (d > 0) {
              t += d;
              if ( (p.length > 0) && p[p.length-1].isOffline && (p[p.length-1].type != 'redirect') ) {
                  p[p.length-1].duration += d;
              } else {
                  p.push( {
                      duration: d,
                      isOffline : true,
                  } );
              }
          }
      }
  
      let pushProgram = (item) => {
          if ( item.isOffline && (item.type !== 'redirect') ) {
              pushFlex(item.duration);
          } else {
              p.push(item);
              t += item.duration;
          }
      };
  
      let slotLastPlayed = {};
  
      while ( (t < hardLimit) && (p.length < LIMIT) ) {
          await throttle();
          //ensure t is padded
          let m = t % schedule.pad;
          if ( (t % schedule.pad > constants.SLACK) && (schedule.pad - m > constants.SLACK) )  {
              pushFlex( schedule.pad - m );
              continue;
          }
  
          let slot = null;
          let slotIndex = null;
          let remaining = null;
  
          let n = 0;
          let minNextTime = t + 24*DAY;
          for (let i = 0; i < s.length; i++) {
              if ( typeof( slotLastPlayed[i] ) !== undefined ) {
                  let lastt = slotLastPlayed[i];
                  minNextTime = Math.min( minNextTime, lastt + s[i].cooldown );
                  if (t - lastt < s[i].cooldown - constants.SLACK ) {
                      continue;
                  }
              }
              n += s[i].weight;
              if ( random.bool(s[i].weight,n) ) {
                  slot = s[i];
                  slotIndex = i;
                  remaining = s[i].duration;
              }
          }
          if (slot == null) {
              //Nothing to play, likely due to cooldown
              pushFlex( minNextTime - t);
              continue;
          }
          let item = getNextForSlot(slot, remaining);
  
          if (item.isOffline) {
              //flex or redirect. We can just use the whole duration
              item.duration = remaining;
              pushProgram(item);
              slotLastPlayed[ slotIndex ] = t;
              continue;
          }
          if (item.duration > remaining) {
              // Slide
              pushProgram(item);
              slotLastPlayed[ slotIndex ] = t;
              advanceSlot(slot);
              continue;
          }
  
          let padded = makePadded(item);
          let total = padded.totalDuration;
          advanceSlot(slot);
          let pads = [ padded ];
  
          while(true) {
              let item2 = getNextForSlot(slot);
              if (total + item2.duration > remaining) {
                  break;
              }
              let padded2 = makePadded(item2);
              pads.push(padded2);
              advanceSlot(slot);
              total += padded2.totalDuration;
          }
          let temt = t + total;
          let rem = 0;
          if (
              (temt % schedule.pad >= constants.SLACK)
              && (temt % schedule.pad < schedule.pad - constants.SLACK)
          ) {
              rem = schedule.pad - temt % schedule.pad;
          }
          
  
          if (flexBetween && (schedule.padStyle === 'episode') ) {
              let div = Math.floor(rem / schedule.pad );
              let mod = rem % schedule.pad;
              // add mod to the latest item
              pads[ pads.length - 1].pad += mod;
              pads[ pads.length - 1].totalDuration += mod;
  
              let sortedPads = pads.map( (p, $index) => {
                  return {
                      pad: p.pad,
                      index : $index,
                  }
              });
              sortedPads.sort( (a,b) => { return a.pad - b.pad; } );
              for (let i = 0; i < pads.length; i++) {
                  let q = Math.floor( div / pads.length );
                  if (i < div % pads.length) {
                      q++;
                  }
                  let j = sortedPads[i].index;
                  pads[j].pad += q * schedule.pad;
              }
          } else if (flexBetween) {
              //just distribute it equitatively
              let div = rem / pads.length;
              for (let i = 0; i < pads.length; i++) {
                  pads[i].pad += div;
              }
          } else {
              //also add div to the latest item
              pads[ pads.length - 1].pad += rem;
              pads[ pads.length - 1].totalDuration += rem;
          }
          // now unroll them all
          for (let i = 0; i < pads.length; i++) {
              pushProgram( pads[i].item );
              slotLastPlayed[ slotIndex ] = t;
              pushFlex( pads[i].pad );
          }
      }
      while ( (t > hardLimit) || (p.length >= LIMIT) ) {
          t -= p.pop().duration;
      }
      let m = (t - t0) % schedule.period;
      if (m != 0) {
          //ensure the schedule is a multiple of period
          pushFlex( schedule.period - m);
      }
  
  
      return {
          programs: p,
          startTime: (new Date(t0)).toISOString(),
      }
  
  }
  
  
  
  
  