// IBAM, the Intelligent Battery Monitor
// Copyright (C) 2001-2003, Sebastian Ritterbusch (IBAM@Ritterbusch.de)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//

#include <iostream>
#include <fstream>
#include <string>
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <locale.h> // thus I may prevent evil krells to change to others.. 

#include <sys/stat.h>  // for mkdir
#include <sys/types.h> // for mkdir

/* Arguments, with symbols from linux/apm_bios.h.  Information is
+          from the Get Power Status (0x0a) call unless otherwise noted.
+
+          0) Linux driver version (this will change if format changes)
+          1) APM BIOS Version.  Usually 1.0 or 1.1.
+          2) APM flags from APM Installation Check (0x00):
+             bit 0: APM_16_BIT_SUPPORT
+             bit 1: APM_32_BIT_SUPPORT
+             bit 2: APM_IDLE_SLOWS_CLOCK
+             bit 3: APM_BIOS_DISABLED
+             bit 4: APM_BIOS_DISENGAGED
+          3) AC line status
+             0x00: Off-line
+             0x01: On-line
+             0x02: On backup power (APM BIOS 1.1 only)
+             0xff: Unknown
+          4) Battery status
+             0x00: High
+             0x01: Low
+             0x02: Critical
+             0x03: Charging
+             0xff: Unknown
+          5) Battery flag
+             bit 0: High
+             bit 1: Low
+             bit 2: Critical
+             bit 3: Charging
+             bit 7: No system battery
+             0xff: Unknown
+          6) Remaining battery life (percentage of charge):
+             0-100: valid
+             -1: Unknown
+          7) Remaining battery life (time units):
+             Number of remaining minutes or seconds
+             -1: Unknown
+          8) min = minutes; sec = seconds */

inline int apm_status::onBattery(void) const { return acLineStatus==0; }
inline int apm_status::charging(void)  const { return (batteryFlag&8)!=0; }
inline int apm_status::percent(void)   const { return remainingBatteryPercent; }
inline int apm_status::seconds(void)   const { return remainingBatteryLifeSeconds; } 
   
inline void apm_status::update(char *path)
{
   ifstream in;
   int i;
   in.open(path);
   for(i=0;i<10 && in.fail();i++)
      in.open(path);
   if(in.fail())
   {
      driverVersion="";
      biosVersion="";
      apmFlags=0;
      acLineStatus=0;
      batteryStatus=0;
      batteryFlag=0;
      remainingBatteryPercent=-1;
      remainingBatteryLifeSeconds=-1;
      return;
   }
   char c,d;
   in >> driverVersion;
   in >> biosVersion;
   in >> c >> d; // 0x
   in >> c >> d;
   apmFlags=(c>'9'?c-'a'+10:c-'0')*16+(d>'9'?d-'a'+10:d-'0');
   in >> c >> d; // 0x
   in >> c >> d;
   acLineStatus=(c>'9'?c-'a'+10:c-'0')*16+(d>'9'?d-'a'+10:d-'0');
   in >> c >> d; // 0x
   in >> c >> d;
   batteryStatus=(c>'9'?c-'a'+10:c-'0')*16+(d>'9'?d-'a'+10:d-'0');
   in >> c >> d; // 0x
   in >> c >> d;
   batteryFlag=(c>'9'?c-'a'+10:c-'0')*16+(d>'9'?d-'a'+10:d-'0');
   in >> remainingBatteryPercent >> c; // %
   string minsec;
   in >> remainingBatteryLifeSeconds >> minsec;
   if(minsec=="min") remainingBatteryLifeSeconds*=60;
#ifdef DEBUG
   cout << "Driver Version:    " << driverVersion << endl;
   cout << "Bios Version:      " << biosVersion << endl;
   cout << "APM Flags:         " << apmFlags << endl;
   cout << "AC Line Status:    " << acLineStatus << endl;
   cout << "Battery Status:    " << batteryStatus << endl;
   cout << "Battery Flag:      " << batteryFlag << endl;
   cout << "Remaining Percent: " << remainingBatteryPercent << endl;
   cout << "Remaining Seconds: " << remainingBatteryLifeSeconds << endl;
#endif
}

inline apm_status::apm_status(char *path)
{
   update(path);
}

inline void percent_data::size_to(int newpercents)
{
   if(newpercents>=maxpercents)
   {
      newpercents++;
      double *time_for=new double[newpercents];
      double *time_deriv=new double[newpercents];
      int    *samples=new int[newpercents];
      int i;
      for(i=0;i<maxpercents;i++)
      {
         time_for[i]=time_for_percent[i];
         time_deriv[i]=time_deriv_for_percent[i];
         samples[i]=time_samples[i];
      }
      for(;i<newpercents;i++)
         time_for[i]=time_deriv[i]=samples[i]=0;
      
      delete [] time_for_percent;
      delete [] time_deriv_for_percent;
      delete [] time_samples;
      time_for_percent=time_for;
      time_deriv_for_percent=time_deriv;
      time_samples=samples;
      maxpercents=newpercents;
   }
}   
   
inline percent_data::percent_data(void) : maxpercents(101), 
                  time_for_percent(new double[maxpercents]),
                  time_deriv_for_percent(new double[maxpercents]),
                  time_samples(new int[maxpercents])
{
   int i;
   for(i=0;i<maxpercents;i++)
      time_for_percent[i]=time_deriv_for_percent[i]=time_samples[i]=0;
}
inline percent_data::~percent_data(void)
{
   delete [] time_for_percent;
   delete [] time_deriv_for_percent;
   delete [] time_samples;
}
inline ostream & operator <<(ostream & o,const percent_data & a)
{
   int i;
   setlocale(LC_ALL,"en_US");
   for(i=a.maxpercents-1;i>=0;i--)
      if(a.time_samples[i])
      {
         if(a.time_deriv_for_percent[i]<0)
            a.time_deriv_for_percent[i]=0;
         o << i << '\t' << a.time_for_percent[i] << '\t' << sqrt(a.time_deriv_for_percent[i]) << '\t' << a.time_samples[i] << endl;
      }
   return o;
}
inline double percent_data::add_data(int percent,double time_for,int samples)
{
   if(percent<0)
      return 0;
   size_to(percent);
   double ratio;
   if(time_samples[percent])
      ratio=time_for/time_for_percent[percent];
   else
      ratio=time_for/(IBAM_ASSUME_DEFAULT_BATTERY_MIN*60./100.);
      
   double old_time_for_percent=time_for_percent[percent];
      
   time_for_percent[percent]=
    (time_for_percent[percent]*time_samples[percent]
    +time_for*samples
    )/(samples+time_samples[percent]);
    
   time_deriv_for_percent[percent]=
    ( (time_deriv_for_percent[percent]+old_time_for_percent*old_time_for_percent)*time_samples[percent]
    +time_for*time_for*samples
    )/(samples+time_samples[percent])-time_for_percent[percent]*time_for_percent[percent];
    
   time_samples[percent]+=samples;
   return ratio;
}
inline double percent_data::average(int a,int b) // average from a to b
{
   if(a>b) { int c=a;a=b;b=c; }
   if(a<0)
   {
      a=0;
      if(b<0)
         b=0;
   }
   if(b>=maxpercents)
   {
      b=maxpercents-1;
      if(a>=maxpercents)
         a=b;
   }
   int i;
   double su(0);
   int    co(0);
   for(i=a;i<=b;i++)
   {
      if(time_samples[i])
      {
         su+=time_for_percent[i]*time_samples[i];
         co+=time_samples[i];
      }
   }
   if(co)
      return (su/co);
   int gotdata=0;
   for(a--,b++;(a>0 || b<maxpercents-1) && gotdata<2;a--,b++)
   {
      if(a<0)
         a=0;
      if(b>=maxpercents)
         b=maxpercents-1;
      su+=time_for_percent[a]*time_samples[a]
          +time_for_percent[b]*time_samples[b];
      co+=time_samples[a]+time_samples[b];
      if(time_samples[a] || time_samples[b])
         gotdata++;
   }
   if(co)
      return (su/co);

   return (IBAM_ASSUME_DEFAULT_BATTERY_MIN*60/100);
}

inline double percent_data::add_data(int percent,double time_for,double time_deriv_for,int samples)
{
   if(percent<0)
      return 0;
   size_to(percent);
   double ratio;
   if(time_samples[percent])
      ratio=time_for/time_for_percent[percent];
   else
      ratio=time_for/average(percent,percent);
      
   double old_time_for_percent=time_for_percent[percent];
      
   time_for_percent[percent]=
    (time_for_percent[percent]*time_samples[percent]
    +time_for*samples
    )/(samples+time_samples[percent]);
    
   time_deriv_for_percent[percent]=
    ( (time_deriv_for_percent[percent]+old_time_for_percent*old_time_for_percent)*time_samples[percent]
    + (time_deriv_for+time_for*time_for)*samples
    )/(samples+time_samples[percent])-time_for_percent[percent]*time_for_percent[percent];
    
   time_samples[percent]+=samples;
   return ratio;
}
inline istream & operator >>(istream & i,percent_data &a)
{
   setlocale(LC_ALL,"en_US");

   while(!i.fail() && !i.eof())
   {
      int percent;
      double time_for(-1);
      double time_deriv_for(-1);
      int samples;
      i >> percent >> time_for >> time_deriv_for>> samples;
      if(time_for>=0)
         a.add_data(percent,time_for,time_deriv_for*time_deriv_for,samples);
   }
   return i;
}
inline istream & percent_data::import(istream & i)
{
   setlocale(LC_ALL,"en_US");

   percent_data & a(*this);
   double maxval=0;
   while(!i.fail() && !i.eof())
   {
      int val;
      double time_for(-1);
      int samples;
      i >> val >> time_for >> samples;
      if(val>maxval)
         maxval=val;
      if(time_for>=0)
         a.add_data(int(double(val)/maxval*100+.5),time_for*maxval/100,samples/10+1);
   }
   return i;
}
inline double percent_data::remain(int percent)
{
   double r=0;
   size_to(percent);
   int i;
   for(i=percent;i>0;i--)
   {
      if(time_samples[i])
         r+=time_for_percent[i];
      else
      {
         int down=i-15;
         int up=i+15;
         if(down<0)
            down=0;
         if(up>=maxpercents)
            up=maxpercents-1;
         r+=average(down,up);
      }
   }
   return r;
}
inline double percent_data::inverted_remain(int percent)
{
   double r=0;
   size_to(percent);
   int i;
   for(i=percent+1;i<maxpercents;i++)
   {
      if(time_samples[i])
         r+=time_for_percent[i];
      else
      {
         int down=i-15;
         int up=i+15;
         if(down<0)
            down=0;
         if(up>=maxpercents)
            up=maxpercents-1;
         r+=average(down,up);
      }
   }
   return r;
}
inline double percent_data::total(void)
{
   double r=0;
   int i;
   for(i=maxpercents-1;i>0;i--)
   {
      if(time_samples[i])
         r+=time_for_percent[i];
      else
      {
         int down=i-15;
         int up=i+15;
         if(down<0)
            down=0;
         if(up>=maxpercents)
            up=maxpercents-1;
         r+=average(down,up);
      }
   }
   return r;
}

inline ibam::ibam(void) : 
             data_changed(0),
             battery_loaded(0),battery_changed(0),
             charge_loaded(0),charge_changed(0),
             profile_changed(0),adaptive_damping_battery(15),
             adaptive_damping_charge(15),
             lasttime(time(NULL)),lastpercent(0),lastratio(1),
             laststatus(-1),
             currenttime(time(NULL)),isvalid(0),profile_logging(1),
             profile_number(0),profile_active(0)
{
   home=getenv("HOME");
   if(home!="")
      home+="/";
   mkdir((home+".ibam").c_str(),0755);
   ifstream in((home+".ibam/ibam.rc").c_str());
   string saveversion;
   in >> saveversion;
   if(saveversion==IBAM_VERSION)
      in >> lasttime >> lastpercent >> lastratio >> laststatus >> adaptive_damping_battery >> adaptive_damping_charge >> profile_logging >> profile_number >> profile_active;
   else
      data_changed=1; // force update
   in.close();
   
   if(adaptive_damping_battery<2)
      adaptive_damping_battery=2;
   if(adaptive_damping_charge<2)
      adaptive_damping_charge=2;
   if(adaptive_damping_battery>200)
      adaptive_damping_battery=200;
   if(adaptive_damping_charge>200)
      adaptive_damping_charge=200;
   
   currentpercent=apm.percent();
   if(currentpercent!=-1)
      isvalid=1;

   currentstatus=apm.onBattery()?1:apm.charging()?2:0;

   if(currentstatus!=laststatus)
      lastratio=1;
}

inline void ibam::update(void)
{
   save();
   apm.update();
   currenttime=time(NULL);
   currentpercent=apm.percent();
   if(currentpercent!=-1)
      isvalid=1;
   else
      isvalid=0;
      
   currentstatus=apm.onBattery()?1:apm.charging()?2:0;

   if(currentstatus!=laststatus)
      lastratio=1;
}

inline int  ibam::valid(void) const { return isvalid; }

inline void ibam::import(void)
{
   {
      ifstream in(".ibam.battery.rc");
      battery.import(in);
      battery_changed=1;
   }
   {
      ifstream in(".ibam.charge.rc");
      charge.import(in);
      charge_changed=1;
   }
}

inline void ibam::load_battery(void)
{
   if(!battery_loaded)
   {
      ifstream in((home+".ibam/battery.rc").c_str());
      in >> battery;
      battery_loaded=1;
   }
}

inline void ibam::load_charge(void)
{
   if(!charge_loaded)
   {
      ifstream in((home+".ibam/charge.rc").c_str());
      in >> charge;
      charge_loaded=1;
   }
}

inline void ibam::update_statistics(void)
{
   if(currentstatus==laststatus && 
      currenttime-lasttime<IBAM_IGNORE_DATA_AFTER_X_SECONDS)
   {
      switch(currentstatus)
      {
         case 1: // on battery
            if(currentpercent<lastpercent)
            {
               load_battery();
               double sec_per_min=(currenttime-lasttime)/double(lastpercent-currentpercent);
               double last_av=battery.average(currentpercent,lastpercent);
               
               if(fabs(last_av-sec_per_min)<1.01*fabs(last_av*lastratio-sec_per_min))
               {
                  if((lastratio<1 && last_av<sec_per_min)
                  || (lastratio>1 && last_av>sec_per_min))
                     adaptive_damping_battery*=1.01;
                  else
                     adaptive_damping_battery*=.99;
               }
               if(sec_per_min>=IBAM_MINIMAL_SECONDS_PER_PERCENT 
               && sec_per_min<=IBAM_MAXIMAL_SECONDS_PER_PERCENT)
               {
                  int i;
                  last_sec_per_min=sec_per_min;
                  last_sec_per_min_prediction=last_av;
                  profile_changed=1;
                  
                  for(i=currentpercent;i<=lastpercent;i++)
                     lastratio=(lastratio*adaptive_damping_battery+battery.add_data(i,sec_per_min))/(adaptive_damping_battery+1);
                  battery_changed=1;
                  data_changed=1;
               }              
            } else
            if(currentpercent>lastpercent) // strange data
            {
               data_changed=1; // discard
               if(profile_logging && profile_active)
                  profile_number++;
               profile_active=0;
            }
            break;
         case 2: // charging
            if(currentpercent>lastpercent)
            {
               load_charge();
               double sec_per_min;
               sec_per_min=(currenttime-lasttime)/double(currentpercent-lastpercent);
               double last_av=charge.average(lastpercent,currentpercent);
               
               if(fabs(last_av-sec_per_min)<1.01*fabs(last_av/lastratio-sec_per_min))
               {
                  if((lastratio>1 && last_av<sec_per_min)
                  || (lastratio<1 && last_av>sec_per_min))
                     adaptive_damping_charge*=1.01;
                  else
                     adaptive_damping_charge*=.99;
               }
                
               if(sec_per_min<=IBAM_MAXIMAL_SECONDS_PER_PERCENT 
               && sec_per_min>=IBAM_MINIMAL_SECONDS_PER_PERCENT)
               {
                  int i;
                  last_sec_per_min=sec_per_min;
                  last_sec_per_min_prediction=last_av;
                  profile_changed=1;
                  
                  for(i=currentpercent;i>=lastpercent;i--)
                     lastratio=(lastratio*adaptive_damping_charge+1/charge.add_data(i,sec_per_min))/(adaptive_damping_charge+1);
                  charge_changed=1;
                  data_changed=1;
               }
            } else
            if(currentpercent<lastpercent) // strange data
            {
               if(profile_logging && profile_active)
                  profile_number++;
               profile_active=0;

               data_changed=1; // discard
            }
            break;
         default: // full or no battery
            break;
      }
   } else
   {
      if(profile_logging && profile_active)
         profile_number++;
      profile_active=0;
      data_changed=1;
   }
}

inline void ibam::ignore_statistics(void)
{
   data_changed=1;
}

inline string ibam::profile_filename(int n,int type) const
{
   char b[20];
   char *status_text[4]={"full","battery","charge",""};
   sprintf(b,"profile-%03d-%s",n,status_text[(type&3)]);
   return (home+".ibam/")+b;
}

inline int   ibam::current_profile_number(void) const { return profile_number; }
inline int   ibam::current_profile_type(void) const { return currentstatus; }

inline void ibam::save(void)
{
   if(profile_changed && profile_logging)
   {
      profile_number%=IBAM_MAXIMAL_PROFILES;
      string filename=profile_filename(profile_number,currentstatus);
      ofstream out(filename.c_str(),ios::app);
      out << currentpercent << '\t' << last_sec_per_min << '\t' << last_sec_per_min_prediction << endl;
      if(profile_active==0)
         data_changed=1;
      profile_active=1;
      profile_changed=0;
   }
   if(battery_changed)
   {
      ofstream out((home+".ibam/battery.rc").c_str());
      out << battery;
      battery_changed=0;
   }
   if(charge_changed)
   {
      ofstream out((home+".ibam/charge.rc").c_str());
      out << charge;
      charge_changed=0;
   }
   if(data_changed)
   {
      ofstream out((home+".ibam/ibam.rc").c_str());
      out << IBAM_VERSION << '\t' << currenttime 
          << '\t' << currentpercent << '\t' 
          << lastratio << '\t' << currentstatus 
          << '\t' << adaptive_damping_battery
          << '\t' << adaptive_damping_charge  
          << '\t' << profile_logging 
          << '\t' << profile_number 
          << '\t' << profile_active << endl;
      lasttime=currenttime;
      lastpercent=currentpercent;
      laststatus=currentstatus;
      data_changed=0;
   }
}

inline void  ibam::set_profile_logging(int a)          { data_changed=(a!=profile_logging);profile_logging=a; }
inline int   ibam::profile_logging_setting(void) const { return profile_logging; }
      
inline int   ibam::seconds_left_battery_bios(void)     { return apm.seconds(); }
inline int   ibam::seconds_left_battery(void)          { load_battery(); return int(battery.remain(currentpercent)+.5); }
inline int   ibam::seconds_left_battery_adaptive(void) { load_battery(); return int(battery.remain(currentpercent)*lastratio+.5); }

inline int   ibam::percent_battery_bios(void)          { return apm.percent(); }
inline int   ibam::percent_battery(void)               { load_battery(); return int((100.*seconds_left_battery())/battery.total()+.5); }
            
inline int   ibam::seconds_left_charge(void)           { load_charge(); return int(charge.inverted_remain(currentpercent)+.5); }
inline int   ibam::seconds_left_charge_adaptive(void)  { load_charge(); return int(charge.inverted_remain(currentpercent)/lastratio+.5); }

inline int   ibam::percent_charge(void)                { load_charge(); return 100-int((100.*seconds_left_charge())/charge.total()+.5); }

inline int   ibam::seconds_battery_total(void)         { load_battery(); return int(battery.total()+.5); }
inline int   ibam::seconds_battery_total_adaptive(void){ load_battery(); return int(battery.total()*lastratio+.5); }

inline int   ibam::seconds_charge_total(void)          { load_charge(); return int(charge.total()+.5); }
inline int   ibam::seconds_charge_total_adaptive(void) { load_charge(); return int(charge.total()/lastratio+.5); }

inline int   ibam::seconds_battery_correction(void)
{
   if(currentstatus!=laststatus || currentstatus==0
    || lastpercent!=currentpercent)
      return 0;
   if(currentstatus==1)
      return lasttime-currenttime;
   load_battery();
   load_charge();
   return int((currenttime-lasttime)*(battery.average(currentpercent-1,currentpercent+1)/charge.average(currentpercent-1,currentpercent+1))+.5);
}

inline int   ibam::seconds_charge_correction(void)
{
   if(currentstatus!=laststatus || currentstatus==0
    || lastpercent!=currentpercent)
      return 0;
   if(currentstatus==2)
      return lasttime-currenttime;
   load_battery();
   load_charge();
   return int((currenttime-lasttime)/(battery.average(currentpercent-1,currentpercent+1)/charge.average(currentpercent-1,currentpercent+1))+.5);
}

inline int   ibam::onBattery(void) { return apm.onBattery(); }
inline int   ibam::charging(void)  { return apm.charging(); }

inline double percent_data::average_derivation(int a,int b) // average standard derivation from a to b
{
   if(a>b) { int c=a;a=b;b=c; }
   if(a<0)
   {
      a=0;
      if(b<0)
         b=0;
   }
   if(b>=maxpercents)
   {
      b=maxpercents-1;
      if(a>=maxpercents)
         a=b;
   }
   int i;
   double su(0);
   int    co(0);
   for(i=a;i<=b;i++)
   {
      if(time_samples[i])
      {
         if(time_deriv_for_percent[i]>0)
            su+=sqrt(time_deriv_for_percent[i])*time_samples[i];
         co+=time_samples[i];
      }
   }
   if(co)
      return (su/co);
   int gotdata=0;
   for(a--,b++;(a>0 || b<maxpercents-1) && gotdata<2;a--,b++)
   {
      if(a<0)
         a=0;
      if(b>=maxpercents)
         b=maxpercents-1;
      if(time_deriv_for_percent[a]>0 && time_samples[a])
         su+=sqrt(time_deriv_for_percent[a])*time_samples[a];
      if(time_deriv_for_percent[b]>0 && time_samples[b])
         su+=sqrt(time_deriv_for_percent[b])*time_samples[b];
      co+=time_samples[a]+time_samples[b];
      if(time_samples[a] || time_samples[b])
         gotdata++;
   }
   if(co)
      return (su/co);

   return 20;
}
