{}
//----- Color functions -----

var
  colorhook:THANDLE;

type
  tqscolor = (
    bkg_norm,fgr_norm,
    bkg_odd ,fgr_odd,
    bkg_dis ,fgr_dis,
    bkg_del ,fgr_del,
    bkg_hid ,fgr_hid,
    bkg_meta,fgr_meta,
    bkg_sub ,fgr_sub
  );
type
  tQSColorRec = record
    color   : TCOLORREF;
    setting : PAnsiChar;
    descr   : PAnsiChar;
  end;
const
  QSColors: array [tqscolor] of tQSColorRec =(
    (color: $00FFFFFF; setting: 'back_norm'; descr: 'Normal background'          ),
    (color: $00000000; setting: 'fore_norm'; descr: 'Normal foreground'          ),
    (color: $00EBE6DE; setting: 'back_odd' ; descr: 'Odd background'             ),
    (color: $00000000; setting: 'fore_odd' ; descr: 'Odd foreground'             ),
    (color: $008080FF; setting: 'back_dis' ; descr: 'Disabled account background'),
    (color: $00000000; setting: 'fore_dis' ; descr: 'Disabled account foreground'),
    (color: $008000FF; setting: 'back_del' ; descr: 'Deleted account background' ),
    (color: $00000000; setting: 'fore_del' ; descr: 'Deleted account foreground' ),
    (color: $0080FFFF; setting: 'back_hid' ; descr: 'Hidden contact background'  ),
    (color: $00000000; setting: 'fore_hid' ; descr: 'Hidden contact foreground'  ),
    (color: $00BAE699; setting: 'back_meta'; descr: 'Metacontact background'     ),
    (color: $00000000; setting: 'fore_meta'; descr: 'Metacontact foreground'     ),
    (color: $00B3CCC1; setting: 'back_sub' ; descr: 'Subcontact background'  ),
    (color: $00000000; setting: 'fore_sub' ; descr: 'Subcontact foreground'  )
  );


procedure RegisterColors;
var
  cid:TColourID;
  i:tqscolor;
begin
  cid.cbSize:=SizeOf(cid);
  cid.flags :=0;
  StrCopy(cid.group,qs_module);
  StrCopy(cid.dbSettingsGroup,qs_module);

  for i:=Low(tqscolor) to High(tqscolor) do
  begin
    StrCopy(cid.name   ,QSColors[i].descr);
    StrCopy(cid.setting,QSColors[i].setting);
    cid.defcolour:=QSColors[i].color;
    cid.order    :=ORD(i);
    ColourRegister(@cid);
  end;
end;

function ColorReload(wParam:WPARAM;lParam:LPARAM):int;cdecl;
var
  cid:TColourID;
  i:tqscolor;
begin
  result:=0;
  cid.cbSize:=SizeOf(cid);
  StrCopy(cid.group,qs_module);
  for i:=Low(tqscolor) to High(tqscolor) do
  begin
    StrCopy(cid.name ,QSColors[i].descr);
    QSColors[i].color:=CallService(MS_COLOUR_GETA,tlparam(@cid),0);
  end;
end;

//----- Item fill -----

function int2strw(i:uint_ptr;signed:bool=false):PWideChar;
var
  buf:array [0..31] of WideChar;
begin
  if signed then
    StrDupW(result,IntToStr(buf,int_ptr(i)))
  else
    StrDupW(result,IntToStr(buf,i));
end;

function int2hexw(i:uint_ptr):PWideChar;
var
  buf:array [0..31] of WideChar;
begin
  StrDupW(result,IntToHex(buf,i));
end;

function BuildLastSeenTime(date:integer):PWideChar;
var
  pc:pWideChar;
  buf:array [0..19] of WideChar;
  year,month,day,hours,min:integer;
begin
  year:=(date div (60*24*31*356))+1980;

  if year<>0 then
  begin
    date:= date mod (60*24*31*356);
    pc:=@buf;

    month:=date div (60*24*31);
    date :=date mod (60*24*31);
    day  :=date div (60*24);
    date :=date mod (60*24);
    hours:=date div 60;
    min  :=date mod 60;

    IntToStr(pc,day,2);

    inc(pc,2);
    pc^:='.'; inc(pc);
    IntToStr(pc,month,2);
    inc(pc,2);
    pc^:='.'; inc(pc);
    IntToStr(pc,year,4);
    inc(pc,4);
    pc^:=' '; inc(pc);
    pc^:='-'; inc(pc);
    pc^:=' '; inc(pc);
    IntToStr(pc,hours,2);
    inc(pc,2);
    pc^:=':'; inc(pc);
    IntToStr(pc,min,2);

    StrDupW(result,@buf);
  end
  else
    result:=nil;
end;

function BuildLastSeenTimeInt(cont:THANDLE;modulename:PAnsiChar):cardinal;
var
  day,month,year,hours,minutes:word;
begin
  year:=DBReadWord(cont,modulename,'Year',0);
  if year<>0 then
  begin
    month  :=DBReadWord(cont,modulename,'Month'  ,0);
    day    :=DBReadWord(cont,modulename,'Day'    ,0);
    hours  :=DBReadWord(cont,modulename,'Hours'  ,0);
    minutes:=DBReadWord(cont,modulename,'Minutes',0);
    result:=minutes+hours*60+day*60*24+month*60*24*31+(year-1980)*60*24*31*356; // was 366
  end
  else
    result:=0;
end;

function IPtoStr(ip:dword):PWideChar;
var
  p:PWideChar;
  buf:array [0..16] of WideChar;
begin
  p:=@buf;
  IntToStr(buf,ip shr 24);
  while p^<>#0 do inc(p); p^:='.'; inc(p);
  IntToStr(p,(ip shr 16) and $FF);
  while p^<>#0 do inc(p); p^:='.'; inc(p);
  IntToStr(p,HIByte(ip));
  while p^<>#0 do inc(p); p^:='.'; inc(p);
  IntToStr(p,LOByte(ip));
  StrDupW(result,buf);
end;

function TimeToStrW(data:dword):PWideChar;
var
  strdatetime:array [0..63] of WideChar;
begin
  TimeZone_ToStringW(data, 'd - t', @strdatetime, Length(strdatetime));
  StrDupW(result,strdatetime);
end;

function FindMeta(hMeta:THANDLE;var MetaNum:WPARAM):LPARAM;
var
  i:integer;
begin
  result:=0;

  for i:=0 to HIGH(FlagBuf) do
  begin
    with FlagBuf[i] do
    begin
      if contact=hMeta then
      begin
        if wparam=0 then // new meta
        begin
          inc(LastMeta);
          wparam :=LastMeta;
          lparam :=0;
        end;
        MetaNum:=wparam;
        inc(lparam);
        result:=lparam;
        break;
      end;
    end;
  end;

end;

function DoMeta(hContact:TMCONTACT):pointer;
var
  pw:pWideChar;
  i:integer;
begin
  result:=nil;

  for i:=0 to HIGH(FlagBuf) do
  begin
    with FlagBuf[i] do
    begin
      if contact=hContact then
      begin
        if (flags and QSF_META)<>0 then // adding new meta count
        begin
          if wparam=0 then
          begin
            inc(LastMeta);
            wparam:=LastMeta;
//            lparam:=0;
          end;
        end
        else if (flags and QSF_SUBMETA)<>0 then
        begin
          lparam:=FindMeta(db_mc_getMeta(hContact),wparam);
        end;

        if wparam>0 then
        begin
          mGetMem(result,32);
          pw:=result;
          pw[0]:='[';
          IntToStr(pw+1,wparam,3);
          pw[4]:=']';
          if lparam>0 then
          begin
            pw[5]:=' ';
            IntToStr(pw+6,lparam);
          end
          else
            pw[5]:=#0;
        end;
        break;
      end;
    end;
  end;

end;

procedure LoadOneItem(hContact:THANDLE;column:pcolumnitem;proto:integer; var res:tQSRec);
var
  lmodule:PAnsiChar;
  pInfo:PWideChar;
  DbEvent:TMEVENT;
  dbei:TDBEVENTINFO;
  data:tSubstData;
begin
  res.data:=uint_ptr(-1);
  mFreeMem(res.text);
  with column^ do
  begin
    case setting_type of
      QST_SCRIPT: begin
        res.text:=ParseVarString(script,hContact);
      end;

      QST_SERVICE: begin
        data.Parameter :=hContact;
        data.LastResult:=0;
        data.ResultType:=ACF_TYPE_NUMBER;
        if ExecuteService(service,data) then
        begin
          case data.ResultType of
            ACF_TYPE_NUMBER: begin
              res.data:=data.LastResult;
              if (service.flags and ACF_FLAG_HEXNUM)<>0 then
                res.text:=int2hexw(data.LastResult)
              else
              begin
                res.text:=int2strw(data.LastResult,(service.flags and ACF_FLAG_SIGNED)<>0);
              end;
            end;
            ACF_TYPE_UNICODE: begin
              StrDupW(res.text,PWideChar(data.LastResult))
            end;
          end;
          ClearSubstData(data);
        end
        else
          res.text:=nil;

      end;

      QST_CONTACTINFO: begin
        pInfo := Contact_GetInfo(cnftype,hContact,nil);
        if pInfo <> nil then
        begin
           StrDupW(res.text,pInfo);
           mir_free(pInfo);
        end;
      end;

      QST_SETTING: begin
        if module<>nil then
          lmodule:=module
        else
          lmodule:=GetProtoName(proto);

        case datatype of
          QSTS_STRING: begin
            res.text:=DBReadUnicode(hContact,lmodule,setting,nil)
          end;

          QSTS_BYTE: begin
            res.data:=DBReadByte(hContact,lmodule,setting,0);
            res.text:=int2strw(res.data);
          end;

          QSTS_WORD: begin
            res.data:=DBReadWord(hContact,lmodule,setting,0);
            res.text:=int2strw(res.data);
          end;

          QSTS_DWORD: begin
            if (module=nil) and (setting=nil) then
            begin
              res.data:=hContact;
              res.text:=int2hexw(res.data);
            end
            else
            begin
              res.data:=DBReadDWord(hContact,lmodule,setting,0);
              res.text:=int2strw(res.data);
            end;
          end;

          QSTS_SIGNED: begin
            res.data:=DBReadDWord(hContact,lmodule,setting,0);
            res.text:=int2strw(res.data,true);
          end;

          QSTS_HEXNUM: begin
            res.data:=DBReadDWord(hContact,lmodule,setting,0);
            res.text:=int2hexw(res.data);
          end;

          QSTS_IP: begin
            res.data:=DBReadDWord(hContact,lmodule,setting,0);
            if res.data<>0 then
              res.text:=IPtoStr(res.data);
          end;

          QSTS_TIMESTAMP: begin
            res.data:=DBReadDWord(hContact,lmodule,setting,0);
            if res.data<>0 then
              res.text:=TimeToStrW(res.data);
          end;
        end;
      end;

      QST_OTHER: 
        case other of
        QSTO_LASTSEEN: begin
          res.data:=BuildLastSeenTimeInt(hContact,'SeenModule');
          res.text:=BuildLastSeenTime(res.data);
        end;

        QSTO_DISPLAYNAME: begin
          StrDupW(res.text,cli^.pfnGetContactDisplayName(hContact, 0));
        end;

        QSTO_LASTEVENT: begin
          DbEvent:=db_event_last(hContact);
          if DbEvent<>0 then
          begin
            ZeroMemory(@dbei,sizeof(dbei));
            dbei.cbSize:=SizeOf(dbei);
            db_event_get(DbEvent, @dbei);
            res.data:=dbei.timestamp;
            res.text:=TimeToStrW(res.data);
          end
          else
            res.data:=0;
        end;

        QSTO_METACONTACT: begin
          res.text:=DoMeta(hContact);
        end;

        QSTO_EVENTCOUNT: begin
          res.data:=db_event_count(hContact);
          res.text:=int2strw(res.data);
        end;
      end;
    end;
  end;
end;

//----- Initial table filling -----

procedure AddContact(num:integer;hContact:TMCONTACT);
var
  col:pcolumnitem;
  tmpstr:array [0..63] of AnsiChar;
  i:integer;
begin
  FillChar(FlagBuf[num],SizeOf(tQSFRec),0);
  with FlagBuf[num] do
  begin
    contact:=hContact;
    flags  :=0;
    i:=IsContactActive(hContact,tmpstr);
    proto:=FindProto(tmpstr);

    case i of
      -2: flags:=flags or QSF_ACCDEL;  // deleted account
      -1: flags:=flags or QSF_ACCOFF;  // disabled account
//      0 : ; // hidden contact
      1 : flags:=flags or QSF_META;    // metacontact
      2 : flags:=flags or QSF_SUBMETA; // subMetacontact
    end;
    if i>0 then
      flags:=flags or QSF_INLIST;      // normal contact

    if (proto=0) or (i<0) then
      status:=ID_STATUS_OFFLINE
    else
      status:=DBReadWord(contact,GetProtoName(proto),'Status',ID_STATUS_OFFLINE);

    for i:=0 to qsopt.numcolumns-1 do
    begin
      col:=@qsopt.columns[i];
      // col.flags must me same as colorder[i].flags
      if (col.flags and COL_ON)<>0 then
        LoadOneItem(contact,col,proto,MainBuf[num,i]);
    end;
  end;

end;

function PrepareToFill:boolean;
var
  cnt,cnt1:integer;
  hContact:TMCONTACT;
  i:integer;
begin
  result:=false;
  if qsopt.numcolumns=0 then
    exit;
  // calculating contacts
  cnt:=CallService(MS_DB_CONTACT_GETCOUNT,0,0);
  if cnt=0 then
    exit;

  result:=true;

  // Allocate mem
  SetLength(MainBuf,cnt,qsopt.numcolumns);
  SetLength(FlagBuf,cnt);

  for i:=0 to cnt-1 do
    FillChar(MainBuf[i][0],qsopt.numcolumns*SizeOf(tQSRec),0);

  for i:=0 to qsopt.numcolumns-1 do
  begin
    with qsopt.columns[i] do
    begin
      if (flags and COL_ON)<>0 then
        flags := flags or COL_INIT;
    end;
  end;

  // filling buffer
  LastMeta:=0;
  cnt1:=0;
  hContact:=db_find_first();
  while hContact<>0 do
  begin
    //!! check account
    AddContact(cnt1,hContact);
    inc(cnt1);
    if cnt1=cnt then break; // additional checking
    hContact:=db_find_next(hContact);
  end;

end;

//----- Status bar -----
type
  pSBDataRecord = ^tSBDataRecord;
  tSBDataRecord = record
    flags :cardinal;
    total :cardinal; // in clist
    found :cardinal; // by pattern
    online:cardinal; // clist online
    liston:cardinal; // pattern online
  end;
  tSBData = array [0..63] of tSBDataRecord;

procedure DrawSBW(const SBData:tSBData);
var
  aPartPos:array [0..63 ] of integer;
  buf     :array [0..255] of WideChar;
  fmtstr  :array [0..255] of WideChar;
  all:integer;
  i,j:integer;
  p,pc,po,pd,poff,pa:PWideChar;
  rc:TRECT;
  dc:HDC;
  icon:HICON;
  protocnt:integer;
begin
  p:=@buf;
  // p:=FormatSimpleW('%i users found (%i) Online: %i',[SBData[0].found,Length(FlagBuf),SBData[0].online]);
  p:=StrEndW(IntToStr(p,SBData[0].found));
  p:=StrCopyEW(p,TranslateW(' users found ('));
  p:=StrEndW(IntToStr(p,Length(FlagBuf)));
  p:=StrCopyEW(p,TranslateW(') Online: '));
  IntToStr(p,SBData[0].online);

  dc:=GetDC(StatusBar);
  DrawTextW(dc,pWidechar(@buf),-1,rc,DT_CALCRECT);
  ReleaseDC(StatusBar,dc);
  all:=rc.right-rc.left;
  aPartPos[0]:=all;
  protocnt:=GetNumProto;
  i:=1;
  while i<=protocnt do
  begin
    inc(all,55);
    aPartPos[i]:=all;
    inc(i);
  end;
  aPartPos[i]:=-1;
  SendMessageW(StatusBar,SB_SETPARTS,protocnt+2,lparam(@aPartPos));
  SendMessageW(StatusBar,SB_SETTEXTW,0,lparam(@buf){p});
  // mFreeMem(p);

  po  :=TranslateW('Online');
  pd  :=TranslateW('deleted');
  poff:=TranslateW('off');
  pa  :=TranslateW('active');

  for i:=1 to protocnt do
  begin
    if ((SBData[i].flags and (QSF_ACCDEL or QSF_ACCOFF))<>0) then
    begin
      icon:=Skin_LoadProtoIcon(nil,ID_STATUS_OFFLINE,0);
    end
    else
    begin
      icon:=Skin_LoadProtoIcon(GetProtoName(i),ID_STATUS_ONLINE,0);
    end;

    FastAnsiToWideBuf(GetProtoName(i),fmtstr);

    SendMessageW(StatusBar,SB_SETICON,i,icon);

    j:=High(buf);//(SizeOf(buf) div SizeOf(WideChar))-1;
    buf[j]:=#0;

    // fill by spaces
    p:=@buf[0];
    while j>0 do
    begin
      dec(j);
      p^:=' ';
      inc(p);
    end;

    if (SBData[i].flags and QSF_ACCDEL)<>0 then
    begin
      buf [0]:='!';
      pc:=pd;
    end
    else if (SBData[i].flags and QSF_ACCOFF)<>0 then
    begin
      buf [0]:='?';
      pc:=poff
    end
    else
      pc:=pa;

    IntToStr(pWideChar(@buf[2]),SBData[i].found);
    StrEndW(buf)^:=' ';
    SendMessageW(StatusBar,SB_SETTEXTW,i,lparam(@buf));

// create tooltip
//  FormatSimpleW('%s (%s): %i (%i); %s %i (%i)',
//                [fmtstr,pc,found,total,po,liston,online]);
    p:=@buf;
    p:=StrCopyEW(p,fmtstr); // Protocol
    p^:=' '; inc(p);
    p^:='('; inc(p);
    p:=StrCopyEW(p,pc);     // Protocol status
    p^:=')'; inc(p);
    p^:=':'; inc(p);
    p^:=' '; inc(p);

    with SBData[i] do
    begin
      p:=StrEndW(IntToStr(p,found));
      p^:=' '; inc(p);
      p^:='('; inc(p);
      p:=StrEndW(IntToStr(p,total));
      p^:=')'; inc(p);
      p^:=';'; inc(p);
      p^:=' '; inc(p);
      p:=StrCopyEW(p,po);
      p^:=' '; inc(p);
      p:=StrEndW(IntToStr(p,liston));
      p^:=' '; inc(p);
      p^:='('; inc(p);
      p:=StrEndW(IntToStr(p,online));
      p^:=')'; inc(p);
    end;
    p^:=#0;
    SendMessageW(StatusBar,SB_SETTIPTEXTW,i,lparam(@buf));
  end;

end;

procedure UpdateSB;
var
  SBData: tSBData;
  j:integer;
  p:pSBDataRecord;
begin
  FillChar(SBData,SizeOf(SBData),0);

  // for all contacts
  for j:=0 to HIGH(FlagBuf) do
  begin
    p:=@SBData[FlagBuf[j].proto];
    p^.flags:=FlagBuf[j].flags;

    inc(p^.total);

    if (p^.flags and QSF_ACTIVE)<>0 then
    begin
      inc(p^.found);
      inc(SBData[0].found);
    end;

    if FlagBuf[j].status<>ID_STATUS_OFFLINE then
    begin
      inc(p^.online);
      inc(SBData[0].online);
      if (p^.flags and QSF_ACTIVE)<>0 then
      begin
        inc(p^.liston);
        inc(SBData[0].liston);
      end;
    end;

  end;

  DrawSBW(SBData);
end;

//----- Patterns -----

const
  pattern:pWideChar = nil; // edit field text
const
  maxpattern = 8;
var
  patterns:array [0..maxpattern-1] of record
    str:PWideChar;
    res:bool;
  end;
const
  patstr:PWideChar=nil; // work pattern buffer
  numpattern:integer=0;

procedure MakePatternW;
var
  lpatptr:PWideChar;
  wasquote:bool;
begin
  numpattern:=0;
  mFreeMem(patstr);
  if (pattern<>nil) and (pattern^<>#0) then
  begin
    wasquote:=false;
    StrDupW(patstr,pattern);
    lpatptr:=patstr;
    repeat
      while lpatptr^=' ' do inc(lpatptr);
      if lpatptr^<>#0 then
      begin
        if lpatptr^='"' then
        begin
          inc(lpatptr);
          wasquote:=true;
        end
        else
        begin
          patterns[numpattern].str:=lpatptr;
          inc(numpattern);
          while lpatptr^<>#0 do
          begin
            if wasquote then
            begin
              if lpatptr^='"' then
              begin
                wasquote:=false;
                break;
              end;
            end
            else if lpatptr^=' ' then
              break;
            inc(lpatptr);
          end;
          if lpatptr^<>#0 then
          begin
            lpatptr^:=#0;
            inc(lpatptr);
          end;
        end;
        if numpattern=maxpattern then break;
      end;
    until lpatptr^=#0;
  end;
end;

function CheckPatternW(cnt:integer):boolean;
var
  lstr:array [0..1023] of WideChar;
  i,j:integer;
begin
  if numpattern>0 then
  begin
    for i:=0 to numpattern-1 do
      patterns[i].res:=false;

    for i:=0 to qsopt.numcolumns-1 do
    begin
      if ((qsopt.columns[i].flags and (COL_ON or COL_FILTER))=(COL_ON or COL_FILTER)) and
         (MainBuf[cnt,i].text<>nil) then
      begin
        StrCopyW(lstr,MainBuf[cnt,i].text,HIGH(lstr));
        CharLowerW(lstr);
        for j:=0 to numpattern-1 do
          if not patterns[j].res then
          begin
            if StrPosW(lstr,patterns[j].str)<>nil then //!!
              patterns[j].res:=true;
          end;
      end;
    end;

    result:=true;
    for i:=0 to numpattern-1 do
      result:=result and patterns[i].res;
  end
  else
    result:=true;
end;

//----- support (column index converters) -----

{
   ListView - ListView (visible) column
   QS       - Buffer column
   Column   - qsopt.columns
}

function ListViewToColumn(col:integer):LPARAM;
var
  i:integer;
begin
  for i:=0 to qsopt.numcolumns-1 do
  begin
    if (qsopt.columns[i].flags and COL_ON)<>0 then
    begin
      dec(col);
      if col<0 then
      begin
        result:=i;
        exit;
      end;
    end;
  end;
  result:=-1;
end;

function ColumnToListView(col:integer):LPARAM;
var
  i:integer;
begin
  result:=-1;
  for i:=0 to qsopt.numcolumns-1 do
  begin
    if (qsopt.columns[i].flags and COL_ON)<>0 then
      inc(result);

    dec(col);
    if col<0 then
      break;
  end;
end;

// return buffer index for contact
function FindBufNumber(hContact:THANDLE):integer;
var
  i:integer;
begin
  for i:=0 to HIGH(FlagBuf) do
  begin
    if FlagBuf[i].contact=hContact then
    begin
      result:=i;
      exit;
    end;
  end;
  result:=-1;
end;

function IsColumnMinimized(num:integer):bool;
begin
  result:=ListView_GetColumnWidth(grid,num)<=10;
end;