content: /*
   * HalfNES by Andrew Hoffman
   * Licensed under the GNU GPL Version 3. See LICENSE file
   */
  package com.grapeshot.halfnes;
  
  import com.grapeshot.halfnes.ui.Oscilloscope;
  import com.grapeshot.halfnes.audio.*;
  import com.grapeshot.halfnes.mappers.Mapper;
  import java.util.ArrayList;
  
  public class APU {
  
      public int samplerate;
      private final Timer[] timers = {new SquareTimer(8, 2), new SquareTimer(8, 2),
          new TriangleTimer(), new NoiseTimer()};
      private double cyclespersample;
      public final NES nes;
      CPU cpu;
      CPURAM cpuram;
      public int sprdma_count;
      private int apucycle = 0, remainder = 0;
      private int[] noiseperiod;
      // different for PAL
      private long accum = 0;
      private final ArrayList<ExpansionSoundChip> expnSound = new ArrayList<>();
      private boolean soundFiltering;
      private final static int[] TNDLOOKUP = initTndLookup(), SQUARELOOKUP = initSquareLookup();
      private int framectrreload;
      private int framectrdiv = 7456;
      private int dckiller = -6392; //removes icky power on thump
      private int lpaccum = 0;
      private boolean apuintflag = true, statusdmcint = false, statusframeint = false;
      private int framectr = 0, ctrmode = 4;
      private final boolean[] lenCtrEnable = {true, true, true, true};
      private final int[] volume = new int[4];
      //dmc instance variables
      private int[] dmcperiods;
      private int dmcrate = 0x36, dmcpos = 0, dmcshiftregister = 0, dmcbuffer = 0,
              dmcvalue = 0, dmcsamplelength = 1, dmcsamplesleft = 0,
              dmcstartaddr = 0xc000, dmcaddr = 0xc000, dmcbitsleft = 8;
      private boolean dmcsilence = true, dmcirq = false, dmcloop = false, dmcBufferEmpty = true;
      //length ctr instance variables
      private final int[] lengthctr = {0, 0, 0, 0};
      private final static int[] lenctrload = {10, 254, 20, 2, 40, 4, 80, 6,
          160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
          192, 24, 72, 26, 16, 28, 32, 30};
      private final boolean[] lenctrHalt = {true, true, true, true};
      //linear counter instance vars
      private int linearctr = 0;
      private int linctrreload = 0;
      private boolean linctrflag = false;
      //instance variables for envelope units
      private final int[] envelopeValue = {15, 15, 15, 15};
      private final int[] envelopeCounter = {0, 0, 0, 0};
      private final int[] envelopePos = {0, 0, 0, 0};
      private final boolean[] envConstVolume = {true, true, true, true};
      private final boolean[] envelopeStartFlag = {false, false, false, false};
      //instance variables for sweep unit
      private final boolean[] sweepenable = {false, false},
              sweepnegate = {false, false},
              sweepsilence = {false, false},
              sweepreload = {false, false};
      private final int[] sweepperiod = {15, 15}, sweepshift = {0, 0}, sweeppos = {0, 0};
      private int cyclesperframe;
      private AudioOutInterface ai;
  
      public APU(final NES nes, final CPU cpu, final CPURAM cpuram) {
          this.samplerate = 1; //just in case we can't init audio
          //then init the audio stream
          this.nes = nes;
          this.cpu = cpu;
          this.cpuram = cpuram;
          setParameters();
      }
  
      private static int[] initTndLookup() {
          int[] lookup = new int[203];
          for (int i = 0; i < lookup.length; ++i) {
              lookup[i] = (int) ((163.67 / (24329.0 / i + 100)) * 49151);
          }
          return lookup;
      }
  
      private static int[] initSquareLookup() {
          //fill square, triangle volume lookup tables
          int[] lookup = new int[31];
          for (int i = 0; i < lookup.length; ++i) {
              lookup[i] = (int) ((95.52 / (8128.0 / i + 100)) * 49151);
          }
          return lookup;
      }
  
      public final synchronized void setParameters() {
          Mapper.TVType tvtype = cpuram.mapper.getTVType();
          soundFiltering = PrefsSingleton.get().getBoolean("soundFiltering", true);
          samplerate = PrefsSingleton.get().getInt("sampleRate", 44100);
          if (ai != null) {
              ai.destroy();
          }
          ai = new SwingAudioImpl(nes, samplerate, tvtype);
          if (PrefsSingleton.get().getBoolean("showScope", false)) {
              ai = new Oscilloscope(ai);
          }
          //pick the appropriate pitches and lengths for NTSC or PAL
          switch (tvtype) {
              case NTSC:
              default:
                  this.dmcperiods = new int[]{428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54};
                  this.noiseperiod = new int[]{4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068};
                  this.framectrreload = 7456;
                  cyclespersample = 1789773.0 / samplerate;
                  cyclesperframe = 29781;
                  break;
  
              case DENDY:
                  this.dmcperiods = new int[]{428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54};
                  this.noiseperiod = new int[]{4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068};
                  this.framectrreload = 7456;
                  cyclespersample = 1773448.0 / samplerate;
                  cyclesperframe = 35469;
                  break;
              case PAL:
                  cyclespersample = 1662607.0 / samplerate;
                  this.dmcperiods = new int[]{398, 354, 316, 298, 276, 236, 210, 198, 176, 148, 132, 118, 98, 78, 66, 50};
                  this.noiseperiod = new int[]{4, 8, 14, 30, 60, 88, 118, 148, 188, 236, 354, 472, 708, 944, 1890, 3778};
                  this.framectrreload = 8312;
                  cyclesperframe = 33252;
                  break;
          }
  //        ai = new Reverberator(ai, 2,0.7,0.8,0.99);
  //        ai = new Reverberator(ai, 243,0.5,0.7,0.99);
  //       ai = new Reverberator(ai, 4001,0.3,0.5,0.99);
  //       ai = new Reverberator(ai, 20382,0.2,0.3,0.9);
      }
  
      public boolean bufferHasLessThan(int samples) {
          return ai.bufferHasLessThan(samples);
      }
  
      public final int read(final int addr) {
          updateto((int) cpu.clocks);
          switch (addr) {
              case 0x15:
                  //returns channel status
                  //for future ref: NEED to put those ternary operators in parentheses!
                  //otherwise order of operations does the wrong thing.
                  final int returnval = ((lengthctr[0] > 0) ? 1 : 0)
                          | ((lengthctr[1] > 0) ? 2 : 0)
                          | ((lengthctr[2] > 0) ? 4 : 0)
                          | ((lengthctr[3] > 0) ? 8 : 0)
                          | ((dmcsamplesleft > 0) ? 16 : 0)
                          | (statusframeint ? 64 : 0)
                          | (statusdmcint ? 128 : 0);
                  if (statusframeint) {
                      //System.err.println("Frame interrupt ack at " + cpu.cycles);
                      --cpu.interrupt;
                      statusframeint = false;
                  }
  
                  //System.err.println("*" + utils.hex(returnval));
                  return returnval;
              case 0x16:
                  nes.getcontroller1().strobe();
                  return nes.getcontroller1().getbyte() | 0x40;
              case 0x17:
                  nes.getcontroller2().strobe();
                  return nes.getcontroller2().getbyte() | 0x40;
              default:
                  return 0x40; //open bus
          }
      }
      final private static int[][] DUTYLOOKUP = {
          {0, 1, 0, 0, 0, 0, 0, 0},
          {0, 1, 1, 0, 0, 0, 0, 0},
          {0, 1, 1, 1, 1, 0, 0, 0},
          {1, 0, 0, 1, 1, 1, 1, 1}
      };
  
      public void addExpnSound(ExpansionSoundChip chip) {
          expnSound.add(chip);
      }
  
      public void destroy() {
          ai.destroy();
      }
  
      public void pause() {
          ai.pause();
      }
  
      public void resume() {
          ai.resume();
      }
  
      public final void write(final int reg, final int data) {
          //This is how values written to any of the APU's memory
          //mapped registers change the state of the system.
          updateto((int) cpu.clocks - 1);
          //System.err.println("Wrote " + utils.hex(data) + " to " + utils.hex(reg) + " @ cycle " + cpu.cycles);
          switch (reg) {
              case 0x0:
                  //length counter 1 halt
                  lenctrHalt[0] = ((data & (utils.BIT5)) != 0);
                  // pulse 1 duty cycle
                  timers[0].setduty(DUTYLOOKUP[data >> 6]);
                  // and envelope
                  envConstVolume[0] = ((data & (utils.BIT4)) != 0);
                  envelopeValue[0] = data & 15;
                  //setvolumes();
                  break;
              case 0x1:
                  //pulse 1 sweep setup
                  //sweep enabled
                  sweepenable[0] = ((data & (utils.BIT7)) != 0);
                  //sweep divider period
                  sweepperiod[0] = (data >> 4) & 7;
                  //sweep negate flag
                  sweepnegate[0] = ((data & (utils.BIT3)) != 0);
                  //sweep shift count
                  sweepshift[0] = (data & 7);
                  sweepreload[0] = true;
                  break;
              case 0x2:
                  // pulse 1 timer low bit
                  timers[0].setperiod((timers[0].getperiod() & 0xfe00) + (data << 1));
                  break;
              case 0x3:
                  // length counter load, timer 1 high bits
                  if (lenCtrEnable[0]) {
                      lengthctr[0] = lenctrload[data >> 3];
                  }
                  timers[0].setperiod((timers[0].getperiod() & 0x1ff) + ((data & 7) << 9));
                  // sequencer restarted
                  timers[0].reset();
                  //envelope also restarted
                  envelopeStartFlag[0] = true;
                  break;
              case 0x4:
                  //length counter 2 halt
                  lenctrHalt[1] = ((data & (utils.BIT5)) != 0);
                  // pulse 2 duty cycle
                  timers[1].setduty(DUTYLOOKUP[data >> 6]);
                  // and envelope
                  envConstVolume[1] = ((data & (utils.BIT4)) != 0);
                  envelopeValue[1] = data & 15;
                  //setvolumes();
                  break;
              case 0x5:
                  //pulse 2 sweep setup
                  //sweep enabled
                  sweepenable[1] = ((data & (utils.BIT7)) != 0);
                  //sweep divider period
                  sweepperiod[1] = (data >> 4) & 7;
                  //sweep negate flag
                  sweepnegate[1] = ((data & (utils.BIT3)) != 0);
                  //sweep shift count
                  sweepshift[1] = (data & 7);
                  sweepreload[1] = true;
                  break;
              case 0x6:
                  // pulse 2 timer low bit
                  timers[1].setperiod((timers[1].getperiod() & 0xfe00) + (data << 1));
                  break;
              case 0x7:
                  if (lenCtrEnable[1]) {
                      lengthctr[1] = lenctrload[data >> 3];
                  }
                  timers[1].setperiod((timers[1].getperiod() & 0x1ff) + ((data & 7) << 9));
                  // sequencer restarted
                  timers[1].reset();
                  //envelope also restarted
                  envelopeStartFlag[1] = true;
                  break;
              case 0x8:
                  //triangle linear counter load
                  linctrreload = data & 0x7f;
                  //and length counter halt
                  lenctrHalt[2] = ((data & (utils.BIT7)) != 0);
                  break;
              case 0x9:
                  break;
              case 0xA:
                  // triangle low bits of timer
                  timers[2].setperiod((((timers[2].getperiod() * 1) & 0xff00) + data));
                  break;
              case 0xB:
                  // triangle length counter load
                  // and high bits of timer
                  if (lenCtrEnable[2]) {
                      lengthctr[2] = lenctrload[data >> 3];
                  }
                  timers[2].setperiod((((timers[2].getperiod() * 1) & 0xff) + ((data & 7) << 8)));
                  linctrflag = true;
                  break;
              case 0xC:
                  //noise halt and envelope
                  lenctrHalt[3] = ((data & (utils.BIT5)) != 0);
                  envConstVolume[3] = ((data & (utils.BIT4)) != 0);
                  envelopeValue[3] = data & 0xf;
                  //setvolumes();
                  break;
              case 0xD:
                  break;
              case 0xE:
                  timers[3].setduty(((data & (utils.BIT7)) != 0) ? 6 : 1);
                  timers[3].setperiod(noiseperiod[data & 15]);
                  break;
              case 0xF:
                  //noise length counter load, envelope restart
                  if (lenCtrEnable[3]) {
                      lengthctr[3] = lenctrload[data >> 3];
                  }
                  envelopeStartFlag[3] = true;
                  break;
              case 0x10:
                  dmcirq = ((data & (utils.BIT7)) != 0);
                  dmcloop = ((data & (utils.BIT6)) != 0);
                  dmcrate = dmcperiods[data & 0xf];
                  if (!dmcirq && statusdmcint) {
                      --cpu.interrupt;
                      statusdmcint = false;
                  }
                  //System.err.println(dmcirq ? "dmc irq on" : "dmc irq off");
                  break;
              case 0x11:
                  dmcvalue = data & 0x7f;
                  break;
              case 0x12:
                  dmcstartaddr = (data << 6) + 0xc000;
                  break;
              case 0x13:
                  dmcsamplelength = (data << 4) + 1;
                  break;
              case 0x14:
                  //sprite dma
                  for (int i = 0; i < 256; ++i) {
                      cpuram.write(0x2004, cpuram.read((data << 8) + i));
                  }
                  //account for time stolen from cpu
                  sprdma_count = 2;
                  break;
              case 0x15:
                  //status register
                  // counter enable(silence channel when bit is off)
                  for (int i = 0; i < 4; ++i) {
                      lenCtrEnable[i] = ((data & (1 << i)) != 0);
                      //THIS was the channels not cutting off bug! If you toggle a channel's
                      //status on and off very quickly then the length counter should
                      //IMMEDIATELY be forced to zero.
                      if (!lenCtrEnable[i]) {
                          lengthctr[i] = 0;
                      }
                  }
                  if (((data & (utils.BIT4)) != 0)) {
                      if (dmcsamplesleft == 0) {
                          restartdmc();
                      }
                  } else {
                      dmcsamplesleft = 0;
                      dmcsilence = true;
                  }
                  if (statusdmcint) {
                      --cpu.interrupt;
                      statusdmcint = false;
                  }
                  break;
              case 0x16:
                  // latch controller 1 + 2
                  nes.getcontroller1().output(((data & (utils.BIT0)) != 0));
                  nes.getcontroller2().output(((data & (utils.BIT0)) != 0));
                  break;
              case 0x17:
                  ctrmode = ((data & (utils.BIT7)) != 0) ? 5 : 4;
                  //System.err.println("reset " + ctrmode + ' ' + cpu.cycles);
                  apuintflag = ((data & (utils.BIT6)) != 0);
                  //set is no interrupt, clear is an interrupt
                  framectr = 0;
                  framectrdiv = framectrreload + 8; //Why +8?
                  if (apuintflag && statusframeint) {
                      statusframeint = false;
                      --cpu.interrupt;
                      //System.err.println("Frame interrupt off at " + cpu.cycles);
                  }
                  if (ctrmode == 5) {
                      //everything frame counter runs is clocked no matter what
                      setenvelope();
                      setlinctr();
                      setlength();
                      setsweep();
                  }
                  break;
              default:
                  break;
          }
      }
  
      public final void updateto(final int cpucycle) {
          //still have to run this even if sound is disabled, some games rely on DMC IRQ etc.
          if (soundFiltering) {
              //linear sampling code
              //should really be a FIR filter + decimator instead
              //but I don't have the DSP experience to design something like that
              //that would be fast enough to work / not require calculating every sample
              //this works well enough at eliminating aliasing anyway.
              while (apucycle < cpucycle) {
                  ++remainder;
                  clockdmc();
                  if (--framectrdiv <= 0) {
                      framectrdiv = framectrreload;
                      clockframecounter();
                  }
                  timers[0].clock();
                  timers[1].clock();
                  if (lengthctr[2] > 0 && linearctr > 0) {
                      timers[2].clock();
                  }
                  timers[3].clock();
                  if (!expnSound.isEmpty()) {
                      for (ExpansionSoundChip c : expnSound) {
                          c.clock(1);
                      }
                  }
                  accum += getOutputLevel();
  
                  if ((apucycle % cyclespersample) < 1) {
                      //not quite right - there's a non-integer # cycles per sample.
                      ai.outputSample(lowpass_filter(highpass_filter((int) (accum / remainder))));
                      remainder = 0;
                      accum = 0;
                  }
                  ++apucycle;
              }
          } else {
              //point sampling code
              while (apucycle < cpucycle) {
                  ++remainder;
                  clockdmc();
                  if (--framectrdiv <= 0) {
                      framectrdiv = framectrreload;
                      clockframecounter();
                  }
                  if ((apucycle % cyclespersample) < 1) {
                      //not quite right - there's a non-integer # cycles per sample.
                      timers[0].clock(remainder);
                      timers[1].clock(remainder);
                      if (lengthctr[2] > 0 && linearctr > 0) {
                          timers[2].clock(remainder);
                      }
                      timers[3].clock(remainder);
                      int mixvol = getOutputLevel();
                      if (!expnSound.isEmpty()) {
                          for (ExpansionSoundChip c : expnSound) {
                              c.clock(remainder);
                          }
                      }
                      remainder = 0;
                      ai.outputSample(lowpass_filter(highpass_filter(mixvol)));
                  }
                  ++apucycle;
              }
          }
      }
  
      private int getOutputLevel() {
          int vol;
          vol = SQUARELOOKUP[volume[0] * timers[0].getval()
                  + volume[1] * timers[1].getval()];
          vol += TNDLOOKUP[3 * timers[2].getval()
                  + 2 * volume[3] * timers[3].getval()
                  + dmcvalue];
          if (!expnSound.isEmpty()) {
              vol *= 0.8;
              for (ExpansionSoundChip c : expnSound) {
                  vol += c.getval();
              }
          }
          return vol; //as usual, lack of unsigned types causes unending pain.
      }
  
      private int highpass_filter(int sample) {
          //for killing the dc in the signal
          sample -= dckiller;
          dckiller += sample >> 8;//the actual high pass part
          dckiller += (sample > 0 ? 1 : -1);//guarantees the signal decays to exactly zero
          return sample;
      }
  
      private int lowpass_filter(int sample) {
          return lpaccum += 0.5 * (sample - lpaccum); //y = y + a * (x - y)
      }
  
      public final void finishframe() {
          updateto(cyclesperframe);
          apucycle = 0;
          ai.flushFrame(nes.isFrameLimiterOn());
      }
  
      private void clockframecounter() {
          //System.err.println("frame ctr clock " + framectr + ' ' + cpu.cycles);
          //should be ~4x a frame, 240 Hz
          //but the problem is this isn't exactly related to the video signal,
          //it's a completely separate timer, so the phase can shift in relation to the
          //video signal. also in the current implementation APU interrupts can only be fired when
          //an APU register is written/read from, or @ end of frame. So both of those need work
          if ((ctrmode == 4)
                  || (ctrmode == 5 && (framectr != 3))) {
              setenvelope();
              setlinctr();
          }
          if ((ctrmode == 4 && (framectr == 1 || framectr == 3))
                  || (ctrmode == 5 && (framectr == 1 || framectr == 4))) {
              setlength();
              setsweep();
          }
          if (!apuintflag && (framectr == 3) && (ctrmode == 4) && !statusframeint) {
              ++cpu.interrupt;
              //System.err.println("frame interrupt set at " + cpu.cycles);
              statusframeint = true;
  
          }
          ++framectr;
          framectr %= ctrmode;
          setvolumes();
      }
  
      private void setvolumes() {
          volume[0] = ((lengthctr[0] <= 0 || sweepsilence[0]) ? 0 : (((envConstVolume[0]) ? envelopeValue[0] : envelopeCounter[0])));
          volume[1] = ((lengthctr[1] <= 0 || sweepsilence[1]) ? 0 : (((envConstVolume[1]) ? envelopeValue[1] : envelopeCounter[1])));
          volume[3] = ((lengthctr[3] <= 0) ? 0 : ((envConstVolume[3]) ? envelopeValue[3] : envelopeCounter[3]));
          //System.err.println("setvolumes " + volume[1]);
      }
  
      private void clockdmc() {
          if (dmcBufferEmpty && dmcsamplesleft > 0) {
              dmcfillbuffer();
          }
          dmcpos = (dmcpos + 1) % dmcrate;
          if (dmcpos == 0) {
              if (dmcbitsleft <= 0) {
                  dmcbitsleft = 8;
                  if (dmcBufferEmpty) {
                      dmcsilence = true;
                  } else {
                      dmcsilence = false;
                      dmcshiftregister = dmcbuffer;
                      dmcBufferEmpty = true;
                  }
              }
              if (!dmcsilence) {
                  dmcvalue += (((dmcshiftregister & (utils.BIT0)) != 0) ? 2 : -2);
                  //DMC output register doesn't wrap around
                  if (dmcvalue > 0x7f) {
                      dmcvalue = 0x7f;
                  }
                  if (dmcvalue < 0) {
                      dmcvalue = 0;
                  }
                  dmcshiftregister >>= 1;
                  --dmcbitsleft;
  
              }
          }
      }
  
      private void dmcfillbuffer() {
          if (dmcsamplesleft > 0) {
              dmcbuffer = cpuram.read(dmcaddr++);
              dmcBufferEmpty = false;
              cpu.stealcycles(4);
              //DPCM Does steal cpu cycles - this should actually vary between 1-4
              //can't do this properly without a cycle accurate cpu/ppu
              if (dmcaddr > 0xffff) {
                  dmcaddr = 0x8000;
              }
              --dmcsamplesleft;
              if (dmcsamplesleft == 0) {
                  if (dmcloop) {
                      restartdmc();
                  } else if (dmcirq && !statusdmcint) {
                      //this is supposed to fire after we've just READ the
                      //last byte, not when coming back AFTER reading the last byte
                      //and finding that there are no more bytes left to read.
                      //that meant all dmc timing was too long.
                      ++cpu.interrupt;
                      statusdmcint = true;
                      //System.err.println("dmc irq fire");
                  }
  
              }
          } else {
              dmcsilence = true;
          }
      }
  
      private void restartdmc() {
          dmcaddr = dmcstartaddr;
          dmcsamplesleft = dmcsamplelength;
          dmcsilence = false;
      }
  
      private void setlength() {
          for (int i = 0; i < 4; ++i) {
              if (!lenctrHalt[i] && lengthctr[i] > 0) {
                  --lengthctr[i];
                  if (lengthctr[i] == 0) {
                      setvolumes();
                  }
              }
          }
      }
  
      private void setlinctr() {
          if (linctrflag) {
              linearctr = linctrreload;
          } else if (linearctr > 0) {
              --linearctr;
          }
          if (!lenctrHalt[2]) {
              linctrflag = false;
          }
      }
  
      private void setenvelope() {
          //System.err.println("envelope");
          for (int i = 0; i < 4; ++i) {
              if (envelopeStartFlag[i]) {
                  envelopeStartFlag[i] = false;
                  envelopePos[i] = envelopeValue[i] + 1;
                  envelopeCounter[i] = 15;
              } else {
                  --envelopePos[i];
              }
              if (envelopePos[i] <= 0) {
                  envelopePos[i] = envelopeValue[i] + 1;
                  if (envelopeCounter[i] > 0) {
                      --envelopeCounter[i];
                  } else if (lenctrHalt[i] && envelopeCounter[i] <= 0) {
                      envelopeCounter[i] = 15;
                  }
              }
          }
      }
  
      private void setsweep() {
          //System.err.println("sweep");
          for (int i = 0; i < 2; ++i) {
              sweepsilence[i] = false;
              if (sweepreload[i]) {
                  sweepreload[i] = false;
                  sweeppos[i] = sweepperiod[i];
              }
              ++sweeppos[i];
              final int rawperiod = (timers[i].getperiod() >> 1);
              int shiftedperiod = (rawperiod >> sweepshift[i]);
              if (sweepnegate[i]) {
                  //invert bits of period
                  //add 1 on second channel only
                  shiftedperiod = -shiftedperiod + i;
              }
              shiftedperiod += rawperiod;
              if ((rawperiod < 8) || shiftedperiod > 0x7ff) {
                  // silence channel
                  sweepsilence[i] = true;
              } else if (sweepenable[i] && (sweepshift[i] != 0) && lengthctr[i] > 0
                      && sweeppos[i] > sweepperiod[i]) {
                  sweeppos[i] = 0;
                  timers[i].setperiod(shiftedperiod << 1);
              }
          }
      }
  }
  