// (c) 2004 Max Howell (max.howell@methylblue.com)
// See COPYING file for licensing information

#include "analyzer.h"
#include "../codeine.h"
#include "../debug.h"
#include <cmath>         //interpolate()
#include <tdeglobalsettings.h>
#include <tqevent.h>     //event()
#include "xineEngine.h"

#include "fht.cpp"

template<class W>
Analyzer::Base<W>::Base( TQWidget *parent, uint timeout, uint scopeSize )
      : W( parent, "Analyzer" )
      , m_timeout( timeout )
      , m_fht(new FHT(scopeSize))
{}

template<class W> void
Analyzer::Base<W>::transform(Scope &scope) //virtual
{
   // This is a standard transformation that should give
   // an FFT scope that has bands for pretty analyzers

   // NOTE: resizing here is redundant as FHT routines only calculate FHT::size() values
   // scope.resize( m_fht->size() );

   float *front = &scope.front();

   auto *f = new float[m_fht->size()];
   m_fht->copy(&f[0], front);
   m_fht->logSpectrum(front, &f[0]);
   m_fht->scale(front, 1.0 / 20);

   scope.resize(m_fht->size() / 2); //second half of values are rubbish
   delete[] f;
}

template<class W>
void Analyzer::Base<W>::drawFrame()
{
   switch(Codeine::engine()->state())
   {
      case Engine::Playing:
      {
         const Engine::Scope &theScope = Codeine::engine()->scope();
         static Scope scope(512);
         int i = 0;

         // Convert to mono.
         // The Analyzer requires mono, but xine reports interleaved PCM.
         for (int x = 0; x < m_fht->size(); ++x)
         {
            // Average between the channels.
            scope[x] = static_cast<double>(theScope[i] + theScope[i + 1]) / (2 * (1 << 15));
            i += 2;
         }

         transform(scope);
         analyze(scope);

         scope.resize(m_fht->size());
         break;
      }
      case Engine::Paused:
      {
         break;
      }
      default:
      {
         demo();
         break;
      }
   }
}

template <class W>
void Analyzer::Base<W>::demo()
{
   static int t = 201; //FIXME make static to namespace perhaps

   if (t > 999)
   {
      // 0 = wasted calculations
      t = 1;
   }
   if (t < 201)
   {
      Scope s(32);

      const auto dt = static_cast<double>(t) / 200.0;
      for (unsigned i = 0; i < s.size(); ++i)
      {
         s[i] = dt * (sin(M_PI + (i * M_PI) / s.size()) + 1.0);
      }
      analyze(s);
   }
   else
   {
      analyze(Scope(32, 0));
   }

   ++t;
}

template<class W> bool
Analyzer::Base<W>::event( TQEvent *e )
{
   switch( e->type() ) {
   case TQEvent::Hide:
      m_timer.stop();
      break;

   case TQEvent::Show:
      m_timer.start( timeout() );
      break;

   default:
      ;
   }

   return TQWidget::event( e );
}


Analyzer::Base2D::Base2D( TQWidget *parent, uint timeout, uint scopeSize )
      : Base<TQWidget>( parent, timeout, scopeSize )
{
   setWFlags( TQt::WNoAutoErase ); //no flicker
   connect( &m_timer, TQ_SIGNAL(timeout()), TQ_SLOT(draw()) );
}

void
Analyzer::Base2D::resizeEvent( TQResizeEvent *e)
{
   m_background.resize(size());
   m_canvas.resize(size());
   m_background.fill(backgroundColor());
   eraseCanvas();

   TQWidget::resizeEvent(e);
}

void Analyzer::Base2D::paletteChange(const TQPalette&)
{
   m_background.fill(backgroundColor());
   eraseCanvas();
}




// Author:    Max Howell <max.howell@methylblue.com>, (C) 2003
// Copyright: See COPYING file that comes with this distribution

#include <tqpainter.h>

Analyzer::Block::Block( TQWidget *parent )
      : Analyzer::Base2D(parent, 20, 9)
      , m_scope(MIN_COLUMNS)
      , m_barPixmap(1, 1)
      , m_topBarPixmap(WIDTH, HEIGHT)
      , m_store(1 << 8, 0)
      , m_fadeBars(FADE_SIZE)
      , m_fadeIntensity(1 << 8, 32)
      , m_fadePos(1 << 8, 50)
      , m_columns(0)
      , m_rows(0)
      , m_y(0)
      , m_step(0)
{
   // -1 is padding, no drawing takes place there
   setMinimumSize(MIN_COLUMNS * (WIDTH + 1) - 1, MIN_ROWS * (HEIGHT + 1) - 1);
   setMaximumWidth(MAX_COLUMNS * (WIDTH + 1) - 1);

   for (auto &m_fadeBar : m_fadeBars)
   {
      m_fadeBar.resize(1, 1);
   }
}

void
Analyzer::Block::transform( Analyzer::Scope &s ) //pure virtual
{
   for( uint x = 0; x < s.size(); ++x )
      s[x] *= 2;

   float *front = static_cast<float*>( &s.front() );

   m_fht->spectrum( front );
   m_fht->scale( front, 1.0 / 20 );

   //the second half is pretty dull, so only show it if the user has a large analyzer
   //by setting to m_scope.size() if large we prevent interpolation of large analyzers, this is good!
   s.resize( m_scope.size() <= MAX_COLUMNS/2 ? MAX_COLUMNS/2 : m_scope.size() );
}


void
Analyzer::Block::analyze( const Analyzer::Scope &s )
{
   // y = 2 3 2 1 0 2
   //     . . . . # .
   //     . . . # # .
   //     # . # # # #
   //     # # # # # #
   //
   // visual aid for how this analyzer works.
   // y represents the number of blanks
   // y starts from the top and increases in units of blocks

   // m_yscale looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
   // if it contains 6 elements there are 5 rows in the analyzer

   interpolate(s, m_scope);

   // Paint the background
   bitBlt(canvas(), 0, 0, background());

   unsigned y;
   for (unsigned x = 0; x < m_scope.size(); ++x)
   {
      if (m_yScale.empty())
      {
         return;
      }

      // determine y
      for (y = 0; m_scope[x] < m_yScale[y]; ++y)
         ;

      // this is opposite to what you'd think, higher than y
      // means the bar is lower than y (physically)
      if (static_cast<float>(y) > m_store[x])
      {
         y = static_cast<int>(m_store[x] += m_step);
      }
      else
      {
         m_store[x] = y;
      }

      // if y is lower than m_fade_pos, then the bar has exceeded the height of the fadeout
      // if the fadeout is quite faded now, then display the new one
      if (y <= m_fadePos[x] /*|| m_fadeIntensity[x] < FADE_SIZE / 3*/ )
      {
         m_fadePos[x] = y;
         m_fadeIntensity[x] = FADE_SIZE;
      }

      if (m_fadeIntensity[x] > 0)
      {
         const unsigned offset = --m_fadeIntensity[x];
         const unsigned y = m_y + (m_fadePos[x] * (HEIGHT + 1));
         bitBlt(canvas(), x * (WIDTH + 1), y, &m_fadeBars[offset], 0, 0, WIDTH, height() - y );
      }

      if (m_fadeIntensity[x] == 0)
      {
         m_fadePos[x] = m_rows;
      }

      // REMEMBER: y is a number from 0 to m_rows, 0 means all blocks are glowing, m_rows means none are
      bitBlt(canvas(), x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, bar(), 0, y * (HEIGHT + 1));
   }

   for (unsigned x = 0; x < m_store.size(); ++x)
   {
      bitBlt(canvas(), x * (WIDTH + 1), int(m_store[x]) * (HEIGHT + 1) + m_y, &m_topBarPixmap);
   }
}


static void adjustToLimits(const int &b, int &f, unsigned &amount)
{
   // with a range of 0-255 and maximum adjustment of amount,
   // maximise the difference between f and b

   if (b < f)
   {
      if (b > 255 - f)
      {
         amount -= f;
         f = 0;
      }
      else
      {
         amount -= (255 - f);
         f = 255;
      }
   }
   else
   {
      if (f > 255 - b)
      {
         amount -= f;
         f = 0;
      }
      else
      {
         amount -= (255 - f);
         f = 255;
      }
   }
}

/**
 * Clever contrast function
 *
 * It will try to adjust the foreground color such that it contrasts well with the background
 * It won't modify the hue of fg unless absolutely necessary
 * @return the adjusted form of fg
 */
TQColor ensureContrast(const TQColor &bg, const TQColor &fg, unsigned _amount = 150)
{
   class OutputOnExit
   {
      public:
         explicit OutputOnExit(const TQColor &color)
            : c(color)
         {
         }

         ~OutputOnExit()
         {
            int h, s, v;
            c.getHsv(&h, &s, &v);
         }

      private:
         const TQColor &c;
   };

   // hack so I don't have to cast everywhere
   #define amount static_cast<int>(_amount)
//     #define STAMP debug() << (TQValueList<int>() << fh << fs << fv) << endl;
//     #define STAMP1( string ) debug() << string << ": " << (TQValueList<int>() << fh << fs << fv) << endl;
//     #define STAMP2( string, value ) debug() << string << "=" << value << ": " << (TQValueList<int>() << fh << fs << fv) << endl;

   OutputOnExit allocateOnTheStack(fg);

   int bh, bs, bv;
   int fh, fs, fv;

   bg.getHsv(&bh, &bs, &bv);
   fg.getHsv(&fh, &fs, &fv);

   int dv = abs(bv - fv);

//     STAMP2( "DV", dv );

   // value is the best measure of contrast
   // if there is enough difference in value already, return fg unchanged
   if (dv > amount)
   {
      return fg;
   }

   int ds = abs(bs - fs);

//     STAMP2( "DS", ds );

   // saturation is good enough too. But not as good. TODO adapt this a little
   if (ds > amount)
   {
      return fg;
   }

   int dh = abs(bh - fh);

//     STAMP2( "DH", dh );

   if (dh > 120)
   {
      // a third of the colour wheel automatically guarantees contrast
      // but only if the values are high enough and saturations significant enough
      // to allow the colours to be visible and not be shades of grey or black

      // check the saturation for the two colours is sufficient that hue alone can
      // provide sufficient contrast
      if (ds > amount / 2 && (bs > 125 && fs > 125))
      {
         // STAMP1( "Sufficient saturation difference, and hues are complimentary" );
         return fg;
      }
      if (dv > amount / 2 && (bv > 125 && fv > 125))
      {
         // STAMP1( "Sufficient value difference, and hues are complimentary" );
         return fg;
      }

      // STAMP1( "Hues are complimentary but we must modify the value or saturation of the contrasting colour" );

      // but either the colours are two desaturated, or too dark
      // so we need to adjust the system, although not as much
      ///_amount /= 2;
    }

   if (fs < 50 && ds < 40)
   {
      // low saturation on a low saturation is sad
      const int tmp = 50 - fs;
      fs = 50;
      if (amount > tmp)
      {
         _amount -= tmp;
      }
      else
      {
         _amount = 0;
      }
   }

   // test that there is available value to honor our contrast requirement
   if (255 - dv < amount)
   {
      // we have to modify the value and saturation of fg
      //adjustToLimits( bv, fv, amount );

      //         STAMP

      // see if we need to adjust the saturation
      if (amount > 0)
      {
         adjustToLimits(bs, fs, _amount);
      }

      //         STAMP

      // see if we need to adjust the hue
      if (amount > 0)
      {
         fh += amount; // cycles around
      }

      //         STAMP

      return TQColor(fh, fs, fv, TQColor::Hsv);
   }

//     STAMP

   if (fv > bv && bv > amount)
   {
      return TQColor( fh, fs, bv - amount, TQColor::Hsv);
   }

//     STAMP

   if (fv < bv && fv > amount)
   {
      return TQColor(fh, fs, fv - amount, TQColor::Hsv);
   }

//     STAMP

   if (fv > bv && (255 - fv > amount))
   {
      return TQColor(fh, fs, fv + amount, TQColor::Hsv);
   }

//     STAMP

   if (fv < bv && (255 - bv > amount))
   {
      return TQColor(fh, fs, bv + amount, TQColor::Hsv);
   }

//     STAMP
//     debug() << "Something went wrong!\n";

   return TQt::blue;

   #undef amount
//     #undef STAMP
}

void Analyzer::Block::paletteChange(const TQPalette&)
{
   const TQColor bg = palette().active().background();
   const TQColor fg = ensureContrast(bg, TDEGlobalSettings::activeTitleColor());

   m_topBarPixmap.fill(fg);

   const double dr = 15 * double(bg.red() - fg.red()) / (m_rows * 16);
   const double dg = 15 * double(bg.green() - fg.green()) / (m_rows * 16);
   const double db = 15 * double(bg.blue() - fg.blue()) / (m_rows * 16);
   const int r = fg.red(), g = fg.green(), b = fg.blue();

   bar()->fill(bg);

   TQPainter p(bar());
   for (int y = 0; (uint)y < m_rows; ++y)
   {
      // graduate the fg color
      p.fillRect(0, y * (HEIGHT + 1), WIDTH, HEIGHT, TQColor(r + int(dr * y), g + int(dg * y), b + int(db * y)));
   }

   {
      const TQColor bg = palette().active().background().dark(112);

      // make a complimentary fadebar colour
      // TODO dark is not always correct, dumbo!
      int h, s, v;
      palette().active().background().dark(150).getHsv(&h, &s, &v);
      const TQColor fg(h + 120, s, v, TQColor::Hsv);

      const double dr = fg.red() - bg.red();
      const double dg = fg.green() - bg.green();
      const double db = fg.blue() - bg.blue();
      const int r = bg.red(), g = bg.green(), b = bg.blue();

      // Precalculate all fade-bar pixmaps
      for (int y = 0; y < FADE_SIZE; ++y)
      {
         m_fadeBars[y].fill(palette().active().background());
         TQPainter f(&m_fadeBars[y]);
         for (int z = 0; (uint)z < m_rows; ++z)
         {
            const double Y = 1.0 - (log10(static_cast<float>(FADE_SIZE) - y) / log10(static_cast<float>(FADE_SIZE)));
            f.fillRect(0, z * (HEIGHT + 1), WIDTH, HEIGHT, TQColor(r + int(dr * Y), g + int(dg * Y), b + int(db * Y)));
         }
      }
   }

   drawBackground();
}


void Analyzer::Block::resizeEvent(TQResizeEvent *e)
{
   TQWidget::resizeEvent(e);

   canvas()->resize(size());
   background()->resize(size());

   const uint oldRows = m_rows;

   // all is explained in analyze()..
   // +1 to counter -1 in maxSizes, trust me we need this!
   m_columns = kMax(uint(double(width() + 1) / (WIDTH + 1)), (uint)MAX_COLUMNS);
   m_rows    = uint(double(height() + 1) / (HEIGHT + 1));

   // this is the y-offset for drawing from the top of the widget
   m_y = (height() - (m_rows * (HEIGHT + 1)) + 2) / 2;

   m_scope.resize(m_columns);

   if (m_rows != oldRows)
   {
      m_barPixmap.resize(WIDTH, m_rows * (HEIGHT + 1));

      for (uint i = 0; i < FADE_SIZE; ++i )
      {
         m_fadeBars[i].resize(WIDTH, m_rows * (HEIGHT + 1));
      }

      m_yScale.resize(m_rows + 1);

      const uint PRE = 1, PRO = 1; //PRE and PRO allow us to restrict the range somewhat

      for (uint z = 0; z < m_rows; ++z)
      {
         m_yScale[z] = 1 - (log10(PRE + z) / log10(PRE + m_rows + PRO));
      }

      m_yScale[m_rows] = 0;

      determineStep();
      paletteChange( palette() );
   }
   else if (width() > e->oldSize().width() || height() > e->oldSize().height())
   {
      drawBackground();
   }

   analyze(m_scope);
}

void Analyzer::Block::determineStep()
{
   // falltime is dependent on rowcount due to our digital resolution (ie we have boxes/blocks of pixels)
   // I calculated the value 30 based on some trial and error

   const double fallTime = 30 * m_rows;

   // The number of milliseconds between signals with audio data is about 80,
   // however, basing the step off of that value causes some undersireable
   // effects in the analyzer (high-end blocks constantly appearing/disappearing).
   // 44 seems to be a good mid-point.
   m_step = double(m_rows * 44) / fallTime;
}

void Analyzer::Block::drawBackground()
{
   const TQColor bg = palette().active().background();
   const TQColor bgdark = bg.dark(112);

   background()->fill(bg);

   TQPainter p(background());
   for (int x = 0; (uint)x < m_columns; ++x)
   {
      for (int y = 0; (uint)y < m_rows; ++y)
      {
         p.fillRect(x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, WIDTH, HEIGHT, bgdark);
      }
   }

   setErasePixmap(*background());
}

void Analyzer::interpolate(const Scope& inVec, Scope& outVec)
{
   double pos = 0.0;
   const double step = (double)inVec.size() / (double)outVec.size();

   for (uint i = 0; i < outVec.size(); ++i, pos += step)
   {
      const double error = pos - std::floor(pos);
      const unsigned long offset = (unsigned long)pos;

      unsigned long indexLeft = offset + 0;

      if (indexLeft >= inVec.size())
      {
         indexLeft = inVec.size() - 1;
      }

      unsigned long indexRight = offset + 1;

      if (indexRight >= inVec.size())
      {
         indexRight = inVec.size() - 1;
      }

      outVec[i] = inVec[indexLeft ] * (1.0 - error) +
                  inVec[indexRight] * error;
   }
}


#include "analyzer.moc"
