unit AcTblFrm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Grids, StdCtrls, ExtCtrls, AcStruc, MTEApi, mteTypes, ComCtrls, CommCtrl,
  acBase, Menus, System.Generics.Defaults, System.Generics.Collections;

type
  PSecProp = ^TSecProp;
  TSecProp = record
    SecCode: string;
    ShortName: string;
    Decimals: Integer;
  end;

  PFirmProp = ^TFirmProp;
  TFirmProp = record
    Code: string;
    ShortName: string;
  end;

  PUserProp = ^TUserProp;
  TUserProp = record
    UserName: string;
    FirmId: string;
    FirmName: string;
  end;

  TIdViewMode = (ivmIds, ivmNames, ivmAll);

  TFastStringList = class(TStringList)
  protected
    function CompareStrings(const S1, S2: string): Integer; override;
  public
    constructor Create;
  end;

  TSecProps = class(TFastStringList)
  private
    function GetProps(AIndex: Integer): PSecProp;
  public
    destructor Destroy; override;
    procedure Clear; override;
    procedure Add(const ASecCode, AShortName: string; ADecimals: Integer); reintroduce;
    property Props[AIndex: Integer]: PSecProp read GetProps; default;
  end;

  TFirmProps = class(TFastStringList)
  private
    function GetProps(AIndex: Integer): PFirmProp;
  public
    destructor Destroy; override;
    procedure Clear; override;
    property Props[AIndex: Integer]: PFirmProp read GetProps; default;
    procedure Add(const AFirmId, AFirmName: string); reintroduce;
  end;

  TUserProps = class(TFastStringList)
  private
    function GetProps(AIndex: Integer): PUserProp;
  public
    destructor Destroy; override;
    procedure Clear; override;
    property Props[AIndex: Integer]: PUserProp read GetProps; default;
    procedure Add(const AUserId, AUserName, AFirmId, AFirmName: string); reintroduce;
  end;

  TMTEConnect = class
  public
    FIdx: Integer;
    FIface: TMicexIface;
    FSysTimeTable: Integer;
    FCaption: string;
    FTree: TTreeView;
    FSecList: TSecProps;
    FDisconnected,
    FLoggedon: Boolean;
    FFirmList,
    FBoardList,
    FSecCodeList: TFirmProps;
    FUserList: TUserProps;
    FParams: TStringList;
    FInitialOpenMode: Boolean;
    fLanguage: string;
    constructor Create;
    destructor Destroy; override;
    procedure SetupKeepAlive;
    function GetErrorMsg(const Err: Integer): string;
  end;

  TTableIndex = class(TFastStringList)
  public
    constructor Create;
    function FindKey(const Key: string; var Index: Integer; SortedByKey: Boolean): Boolean;
    procedure AddKey(InsertPos: Integer; const Key: string; Index: Integer);
    procedure DeleteRow(const Index: Integer; SortedByKey: Boolean);
  end;

  TRowList = class(TList)
  private
    FMsg: TMicexMessage;
    FTableIndex: TTableIndex;
    FLastQuote: array [0..15] of AnsiChar; // SecBoard(4) + SecCode(12)
    FSecCodeOfs: Integer;
    FSecCodeLen: Integer;
    FDecimalsOfs: Integer;
    FDecimalsLen: Integer;
    FIsOrderBook: Boolean;
    FSecList: TSecProps;
    FBadDecimalsStr: string;
    fDecimalsRequired: Boolean;
    function GetValue(ARow, ACol: Integer): string;
    function MakeKeyFromData(const NumsCount: Integer; const NumsPtr, DataPtr: PByte): string;
    function ClearOrderBook(const Data: PByte; const Len: Integer): Boolean;
  public
    constructor Create(AMsg: TMicexMessage; SecList: TSecProps);
    destructor Destroy; override;
    procedure AddRow(var Data: PByte);
    procedure Clear; override;
    property Values[ARow, ACol: Integer]: string read GetValue; default;
    function RowDecimals(const ARow: Integer): Integer;
  end;

  PBaseMsgRec = ^TBaseMsgRec;
  TBaseMsgRec = record
    Msg: TMicexMessage;
    Rows: TRowList;
    RowNum: Integer;
  end;

  TTableForm = class;
  TMappingComparer = class(TComparer<Integer>)
  private
    fTableForm: TTableForm;
  public
    function Compare(const aLeft, aRight: Integer): Integer; override;
    constructor Create(const aTableForm: TTableForm);
  end;

  TTableForm = class(TBaseForm)
    LV: TListView;
    Splitter1: TSplitter;
    pmLink: TPopupMenu;
    miFilter: TMenuItem;
    N1: TMenuItem;
    miDelFilter: TMenuItem;
    miExport: TMenuItem;
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormDestroy(Sender: TObject);
    procedure LVData(Sender: TObject; Item: TListItem);
    procedure pmLinkPopup(Sender: TObject);
    procedure miFilterClick(Sender: TObject);
    procedure miDelFilterClick(Sender: TObject);
    procedure LVDblClick(Sender: TObject);
    procedure LVColumnClick(Sender: TObject; Column: TListColumn);
    procedure miExportClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
//    FIdx: Integer;
    FMsg: TMicexMessage;
    FRows: TRowList;
    FRefreshable: Boolean;
    FTblHandle: Integer;
    FCaption: string;
//    FIface: TMicexIface;
    FConnectCaption: string;
    FConnect: TMTEConnect;
    FFilterValue: string;
    FFilterField, FSortField: TMicexField;
    FMapping: TList<Integer>;
    FFilterFieldIndex, FSortFieldIndex: Integer;
    FFilterIndex: Integer;
    FIfaceName: string;
    FGridLayoutSaved: Boolean;
    FSortDescending: Boolean;
    FSortColumn: TListColumn;
    fSystemIndex: Integer;
    fID: Integer;
    function GetRefreshable: Boolean;
    function SelectedRow: Integer;
    procedure SelectLastRec;
    procedure ExecuteLink(Sender: TObject);
    procedure DoFilter;
    procedure SaveGridLayout;
    function FormatValue(const Value: string; Fld: TMicexField;
      RowIndex: Integer; JustIds, ThousandSeparators: Boolean): string;
    procedure UpdateCaption;
  public
    function AddToPacket(Idx: Integer; out NumMsg: Integer): Integer;
    procedure SetupStatics(AConnect: TMTEConnect; AMsg: TMicexMessage;
      Msg: PMTETable; ATblHandle: Integer; SecList: TSecProps; const AConnectCaption: string;
        aSystemIndex: Integer);
    procedure ParseData(var Msg: PMTETable; ForceRefilter: Boolean = False);
    property Refreshable: Boolean read GetRefreshable;
    procedure UpdateCaptions;
    function ConnectIdx: Integer;
    property ConnectPtr: TMTEConnect read FConnect;
    function SystemIndex: Integer;
  end;

function GetTsmrStr(const Err: Integer; const Msg: PMTEMsg): string;
procedure ShowError(const AConnect: TMTEConnect; const Err: Integer; const ErrMsg, ErrSource: string);
procedure ShowTransExResult(AConnect: TMTEConnect; Err: Integer; Reply: TMTEExecTransResult; ErrSource: string);
procedure DownloadSecurities(Connect: TMTEConnect; const Language: string);
procedure DownloadFirms(Connect: TMTEConnect; const Language: string);
function FindFormByID(const aID: Integer): TTableForm;
function FormatFloatPoint(const S: string): string;
function GetDisplayName(const Obj: TMicexObject; const MainLabel: Boolean): string;

{$IFDEF CLIENTLOG}
var
  DebugFile: Text;
{$ENDIF}

var
  IdViewMode: TIdViewMode = ivmIds;

implementation

uses
  ExtUtils, AcMain, acFilter, IniFiles, Consts, System.UITypes, System.Math;

{$R *.DFM}

function GetDisplayName(const Obj: TMicexObject; const MainLabel: Boolean): string;
begin
  if MainLabel then
    Result := Obj.Name
  else
    Result := Obj.DisplayName;
end;

var
  iSeqNum: Integer;
  FieldSeqNums: array [0..255] of AnsiChar;
  CloseErrorCount: Integer = 0;

const
  SInvalidTime  = '#error!';
  SInvalidDate  = '#error!';
  MAX_COL_WIDTH = 300;

procedure TTableForm.FormClose(Sender: TObject; var Action: TCloseAction);
var
  Err: Integer;
begin
  LV.Items.Count := 0;
  SaveGridLayout;
  Err := MTECloseTable(FConnect.FIdx, FTblHandle);
  if (Err <> MTE_OK) and not FConnect.FDisconnected then
  begin
    // to avoid multiple "not connected" errors, show error just once
    if CloseErrorCount = 0 then ShowError(FConnect, Err, '', 'MTECloseTable');
    Inc(CloseErrorCount);
  end;
  Action := caFree;
end;

function FindFormByID(const aID: Integer): TTableForm;
var
  I: Integer;
begin
  for I := 0 to Screen.FormCount - 1 do
    if (Screen.Forms[I] is TTableForm) and (TTableForm(Screen.Forms[I]).fID = aID) then
    begin
      Result := TTableForm(Screen.Forms[I]);
      Exit;
    end;
  Result := nil;
end;

var
  g_OpenedTableID: Integer = 123;

procedure TTableForm.FormCreate(Sender: TObject);
begin
  inherited;
  fID := InterlockedIncrement(g_OpenedTableID);
end;

procedure TTableForm.SelectLastRec;
begin
  if FRows.Count > 0 then
    with LV.Items[FRows.Count - 1] do
  begin
    MakeVisible(False);
    Selected := True;
    Focused := True;
  end;
end;

procedure TTableForm.ParseData(var Msg: PMTETable; ForceRefilter: Boolean = False);
var
  P: PByte;
begin
  try
    {$IFDEF CLIENTLOG}
    Writeln(DebugFile, '[' + FCaption + ']');
    Writeln(DebugFile, Format('RowCount = %d', [Msg.RowCount]));
    {$ENDIF}

    if Msg.RowCount > 0 then
    begin
      if FMsg.ClearOnUpdate and not FRows.FIsOrderBook then
      begin
        FRows.Clear;
        {$IFDEF CLIENTLOG}
        Writeln(DebugFile, 'FRows.Clear');
        {$ENDIF}
      end;

      FRows.FBadDecimalsStr := '';
      P := @Msg.TblData;
      repeat
        FRows.AddRow(P);
        Dec(Msg.RowCount);
      until Msg.RowCount = 0;
      Msg := PMTETable(P);

      if FRows.FIsOrderBook then
        // prepare FRow for next cycle
        FillChar(FRows.FLastQuote, SizeOf(FRows.FLastQuote), 0);

      DoFilter;
      if FRows.FBadDecimalsStr <> '' then
        MessageDlg(Format('Сервер вернул данные недопустимого формата: поле DECIMALS = "%s"',
          [FRows.FBadDecimalsStr]), mtError, [mbOk], 0);

//      if FRows.FIsOrderBook then UpdateOrderbook(FRows);
    end
    else
    begin
      Msg := @Msg.TblData; // Just skip 8 bytes and move to next table header
      if ForceRefilter then DoFilter;
    end;

(*    with LV, Items do
    begin
      DoScroll := (Count = 0) or (SelectedRow = Count - 1);
      DoFilter;
      if Assigned(FFilterField) then NewCount := FMapping.Count
      else NewCount := FRows.Count;
      if Count <> NewCount then
      begin
        Count := NewCount;
        if DoScroll then SelectLastRec;
      end;
      Invalidate;
    end;

    Caption := Format('%s - %d/%d - %s', [FCaption, NewCount, FRows.Count, FConnectCaption]);*)
  except
    on E: Exception do
    begin
      {$IFDEF CLIENTLOG}
      Writeln(DebugFile, 'EXCEPTION! ', E.ClassName, ': ', E.Message);
      Flush(DebugFile);
      {$ENDIF}
      FRefreshable := False;
      raise;
    end;
  end;

  {$IFDEF CLIENTLOG}
  Flush(DebugFile);
  {$ENDIF}
end;

procedure TTableForm.SetupStatics(AConnect: TMTEConnect;
  AMsg: TMicexMessage; Msg: PMTETable; ATblHandle: Integer; SecList: TSecProps;
    const AConnectCaption: string; aSystemIndex: Integer);
var
  I, DigitWidth, CharWidth, TotalWidth: Integer;
  Fld: TMicexField;
  Section, S: string;
  ColIndices: array of Integer;
  Col: TListColumn;

  function ColumnWidth: Integer;
  var
    CW, CapSize: Integer;
  begin
    if Fld.FieldType = ftChar then CW := CharWidth else CW := DigitWidth;
    Result := CW * Fld.Size;
    if Fld.FieldType = ftDate then Inc(Result, LV.Canvas.TextWidth(FormatSettings.DateSeparator) * 3);
    if Fld.FieldType = ftTime then Inc(Result, LV.Canvas.TextWidth(FormatSettings.TimeSeparator) * 3);
    if Fld.FieldType in [ftFixed, ftFloat] then Inc(Result, LV.Canvas.TextWidth(FormatSettings.DecimalSeparator) * 2);
    CapSize := LV.Canvas.TextWidth(GetDisplayName(Fld, True));
    if CapSize > Result then Result := CapSize;
    if Fld.FieldType = ftChar then Inc(Result, 2);
    Inc(Result, 12);  // 6 left + 6 right spacing
    if Result > MAX_COL_WIDTH then Result := MAX_COL_WIDTH;
  end;

begin
  FConnect := AConnect;
  FMsg := AMsg;
  FConnectCaption := AConnectCaption;
  fSystemIndex := aSystemIndex;
  FRows := TRowList.Create(AMsg, SecList);
  FMapping := TList<Integer>.Create(TMappingComparer.Create(Self));
  FTblHandle := ATblHandle;
  FCaption := Caption;
  FFilterIndex := -1;
  FIfaceName := FConnectCaption;
  for I := 1 to Length(FIfaceName) do
    if CharInSet(FIfaceName[I], ['&', ' ', '[', ']']) then FIfaceName[I] := '_';

  DigitWidth := LV.Canvas.TextWidth('0123456789') div 10;
  CharWidth := LV.Canvas.TextWidth('ABCDEFGHIJKLMNOPQRSTUVWXYZ') div 26;

  LV.DoubleBuffered := True;
  LV.Items.BeginUpdate;
//  LV.Columns.BeginUpdate; -- это не дает задать ширину столбцов под Delphi XE2
  try
    // Delphi doesn't allow to align first column
    with LV.Columns.Add do begin
      Width := 16;      // Add dummy not-alignable column
      Tag := -1;
    end;
    TotalWidth := 20;

    with TIniFile.Create(IniFileName) do
    try
      Section := FIfaceName + '_' + FMsg.Name;

      for I := 0 to AMsg.OutFields.Count - 1 do
      begin
        Fld := AMsg.OutFields[I];
        Col := LV.Columns.Add;
        Col.Caption := GetDisplayName(Fld, True);
        Col.Width := ReadInteger(Section, Fld.Name, ColumnWidth);
        Col.Tag := I;
        Inc(TotalWidth, Col.Width);
        if Fld.FieldType <> ftChar then Col.Alignment := taRightJustify;
      end;

      S := ReadString(Section, '@ColumnOrder', '');
      SetLength(ColIndices, LV.Columns.Count);
      for I := 0 to Length(ColIndices) - 1 do
        ColIndices[I] := StrToIntDef(EWordNum(',', S, I + 1), I);
      ListView_SetColumnOrderArray(LV.Handle, Length(ColIndices), PInteger(ColIndices));
    finally
      Free;
    end;
  finally
//    LV.Columns.EndUpdate; -- это не дает задать ширину столбцов под Delphi XE2
    LV.Items.EndUpdate;
  end;

  if TotalWidth < ClientWidth then ClientWidth := TotalWidth;
  ParseData(Msg, True);
  // Correct form height: header + one extra row for spacing + space for scrollbar
  I := 4 + (FRows.Count + 3) * (LV.Canvas.TextHeight('Wg') + 2);
  if I < ClientHeight then ClientHeight := I;
  FRefreshable := True;
end;

procedure TTableForm.UpdateCaptions;
var
  i: Integer;
  C: TListColumn;
  F: TMicexField;
  S: string;
begin
  LV.Items.BeginUpdate;
  try
    for i := 1 to LV.Columns.Count - 1 do begin // skip first indicator column
      C := LV.Columns[i];
      F := fMsg.OutFields[C.Tag];
      C.Caption := GetDisplayName(F, True);
    end;
  finally
    LV.Items.EndUpdate;
  end;

  i := Pos(' - ', FCaption);
  if i > 0 then begin
    S := FCaption;
    FCaption := GetDisplayName(FMsg, True) + Copy(S, i, MaxInt)
  end else
    FCaption := GetDisplayName(FMsg, True);
  UpdateCaption;
end;

function TTableForm.AddToPacket(Idx: Integer; out NumMsg: Integer): Integer;
begin
  NumMsg := 1;
  Result := MTEAddTable(Idx, FTblHandle, fID);
end;

function TTableForm.GetRefreshable: Boolean;
begin
  Result := Assigned(FMsg) and FMsg.Updateable and FRefreshable;
end;

{ TRowList }

function TRowList.MakeKeyFromData(const NumsCount: Integer; const NumsPtr, DataPtr: PByte): string;
var
  I, FieldIdx, Offset: Integer;
  KeyField: string;
begin
  Result := '';
  Offset := 0;

  for I := 0 to NumsCount - 1 do
  begin
    FieldIdx := NumsPtr[I];
    with FMsg.OutFields[FieldIdx] do
    begin
      if Key then
      begin
        SetString(KeyField, PAnsiChar(DataPtr + Offset), Size);
        Result := Result + KeyField;
      end;
      Inc(Offset, Size);
    end;
  end;
end;

function TRowList.ClearOrderBook(const Data: PByte; const Len: Integer): Boolean;
var
  QuoteLen, I: Integer;
begin
  Result := False;
  QuoteLen := Min(Len, SizeOf(FLastQuote));
  if CompareMem(@FLastQuote, Data, QuoteLen) then Exit; // not first occurence of the quote, do not clear quotes
//  ClearForSecurity(Data);

  {$IFDEF CLIENTLOG}
  Writeln(DebugFile, 'Clear quotes for given security');
  {$ENDIF}

  for I := Count - 1 downto 0 do
  begin
    if CompareMem(Data, Items[I], QuoteLen) then
    begin
      FreeMem(Items[I]);
      Delete(I);
      // delete reference for this row from Index
      FTableIndex.DeleteRow(I, FIsOrderBook);
    end;
  end;
  System.Move(Data^, FLastQuote, QuoteLen);
  Result := Len = SizeOf(FLastQuote);
end;

function GetFieldNums(const NumsPtr: PByte; NumsCount: Integer): string;
var
  I: Integer;
begin
  Result := '';
  for I := 0 to NumsCount - 1 do
    Result := Result + '#' + IntToStr(Byte(NumsPtr[I]));
end;

procedure TRowList.AddRow(var Data: PByte);
var
  H: TMTERow;
  FldNum, RowNum, NumsCount, ReplySize, Decimals: Integer;
  FullRowPtr, NumsPtr, DataPtr: PByte;
  Append, SkipRow: Boolean;
  KeyForData, SecCode: string;
  {$IFDEF CLIENTLOG}
  S: string;
  Len: Integer;
  {$ENDIF}
begin
  System.Move(Data^, H, SizeOf(H));
  Inc(Data, SizeOf(H));

  {$IFDEF CLIENTLOG}
  Len := H.RowLen;
  SetString(S, PAnsiChar(Data + H.FldCount), Len);
  Writeln(DebugFile, Format('Flds=%d(%s),Len=%d,Data=''%s''',
    [H.FldCount, GetFieldNums(Data, H.FldCount), Len, S]));
  {$ENDIF}

  NumsCount := H.FldCount;
  NumsPtr := Data;
  DataPtr := Data + H.FldCount;
  if NumsCount = 0 then
  begin
    NumsCount := FMsg.OutFields.Count;
    NumsPtr := @FieldSeqNums;
  end;

  if FIsOrderBook then// clear quotes for given security?
    SkipRow := ClearOrderBook(DataPtr, H.RowLen)
  else
    SkipRow := False;

  if (not SkipRow) and (H.RowLen > 0) then
  begin
    FullRowPtr := nil;
    with FMsg.OutFields do
    begin
      Append := True;

      if KeyIdx >= 0 then
      begin
        KeyForData := MakeKeyFromData(NumsCount, NumsPtr, DataPtr);
        if FTableIndex.FindKey(KeyForData, RowNum, True{FIsOrderBook}) then
        begin
          Append := False;
          FullRowPtr := Items[RowNum];
        end
        else
          FTableIndex.AddKey(RowNum, KeyForData, Self.Count);
      end
      else
        RowNum := Self.Count;

      ReplySize := SumSize;
      if Append then
      begin
        GetMem(FullRowPtr, ReplySize + SizeOf(Integer)); // + place for Decimals
        FillChar(FullRowPtr^, ReplySize, #32);
        PInteger(@FullRowPtr[ReplySize])^ := 0;
        if True{FIsOrderBook }then
          Self.Insert(RowNum, FullRowPtr)   // sort orderbook by price
        else
          Self.Add(FullRowPtr);
      end;
    end;

    for FldNum := 0 to NumsCount - 1 do
      with FMsg.OutFields[Byte(NumsPtr[FldNum])] do
    begin
      System.Move(DataPtr^, FullRowPtr[Start], Size);
      Inc(DataPtr, Size);
    end;

    if Append then
    begin
      if FDecimalsOfs >= 0 then
      begin
        // Get Decimals from the same row (e.g. INDEXES)
        SetString(SecCode, PAnsiChar(FullRowPtr + FDecimalsOfs), FDecimalsLen);
        Decimals := StrToIntDef(SecCode, -1);  {@@@@@}
        if (Decimals < 0) and fDecimalsRequired then
        begin
          FBadDecimalsStr := SecCode;
          Decimals := 0;
        end;
        PInteger(@FullRowPtr[ReplySize])^ := Decimals;
      end
      else
        if (FSecCodeOfs >= 0) then // calculate Decimals for security in this row
        begin
          SetString(SecCode, PAnsiChar(FullRowPtr + FSecCodeOfs), FSecCodeLen);
          if FSecList.Find(SecCode, RowNum) then
            PInteger(@FullRowPtr[ReplySize])^ := FSecList[RowNum].Decimals;
        end;
    end;

  end;
  Inc(Data, H.FldCount + H.RowLen);
end;

procedure TRowList.Clear;
var
  I: Integer;
begin
  for I := 0 to Count - 1 do FreeMem(Items[I]);
  if Assigned(FTableIndex) then FTableIndex.Clear;
  inherited;
end;

constructor TRowList.Create(AMsg: TMicexMessage; SecList: TSecProps);
var
  I: Integer;
begin
  inherited Create;
  FMsg := AMsg;
  FSecList := SecList;
  FTableIndex := TTableIndex.Create;
  FIsOrderBook := FMsg.IsOrderbook;

  FSecCodeOfs := -1;
  FDecimalsOfs := -1;
  fDecimalsRequired := False;

  I := 0;
  while (I < AMsg.OutFields.Count) and not fDecimalsRequired do begin
    fDecimalsRequired := AMsg.OutFields[I].FieldType = ftFloat;
    Inc(I);
  end;

  with AMsg do
    for I := 0 to OutFields.Count - 1 do
      with OutFields[I] do
        if Name = 'DECIMALS' then
        begin
          FDecimalsOfs := Start;
          FDecimalsLen := Size;
          Exit;
        end;

  with AMsg do
    for I := 0 to OutFields.Count - 1 do
      with OutFields[I] do
        if SecCode then
        begin
          FSecCodeOfs := Start;
          FSecCodeLen := Size;

          if I > 0 then
            with OutFields[I - 1] do
          begin
            Dec(FSecCodeOfs, Size);
            Inc(FSecCodeLen, Size);
          end;
          Break;
        end;
end;

function TRowList.GetValue(ARow, ACol: Integer): string;
begin
  with FMsg.OutFields[ACol] do
    SetString(Result, PAnsiChar(Items[ARow]) + Start, Size);
end;

procedure TTableForm.SaveGridLayout;
var
  Section, S: string;
  I: Integer;
  ColIndices: array of Integer;
begin
  with TIniFile.Create(IniFileName) do
  try
    Section := FIfaceName + '_' + FMsg.Name;
    for I := 1 to LV.Columns.Count - 1 do
      WriteInteger(Section, FMsg.OutFields[LV.Columns[I].Tag].Name,
        Max(3, LV.Columns[I].Width));

    SetLength(ColIndices, LV.Columns.Count);
    ListView_GetColumnOrderArray(LV.Handle, Length(ColIndices), PInteger(ColIndices));
    S := '';
    for I := 0 to Length(ColIndices) - 1 do
    begin
      if S <> '' then S := S + ', ';
      S := S + IntToStr(ColIndices[I]);
    end;
    WriteString(Section, '@ColumnOrder', S);
    FGridLayoutSaved := True;
  finally
    Free;
  end;
end;

procedure TTableForm.FormDestroy(Sender: TObject);
begin
  FRows.Free;
  FMapping.Free;
  if not FGridLayoutSaved then SaveGridLayout;
end;

destructor TRowList.Destroy;
begin
  FTableIndex.Free;
  FTableIndex := nil;
  inherited;
end;

function TRowList.RowDecimals(const ARow: Integer): Integer;
begin
  Result := PInteger(PByte(Items[ARow]) + FMsg.OutFields.SumSize)^;
end;

{ TFastStringList }

function TFastStringList.CompareStrings(const S1, S2: string): Integer;
begin
  Result := CompareStr(S1, S2);
end;

constructor TFastStringList.Create;
begin
  inherited;
  Sorted := True;
end;

{ TTableIndex }

constructor TTableIndex.Create;
begin
  inherited;
  Sorted := False;  // sorted lists does not allow insertion at arbitrary position
end;

procedure TTableIndex.AddKey(InsertPos: Integer; const Key: string; Index: Integer);
begin
  InsertObject(InsertPos, Key, TObject(Index));
end;

procedure TTableIndex.DeleteRow(const Index: Integer; SortedByKey: Boolean);
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
    if (SortedByKey and (I = Index)) or (not SortedByKey and (Index = Integer(Objects[I]))) then
    begin
      Delete(I);
      Break;
    end;
end;

function TTableIndex.FindKey(const Key: string; var Index: Integer; SortedByKey: Boolean): Boolean;
begin
  Result := Find(Key, Index);
  if Result and not SortedByKey then
    Index := Integer(Objects[Index]);
end;

function ConvertStrToDateTime(const S: string; FieldType: TTEFieldType): TDateTime;

  function GetNumber(Buf: PChar): Integer;
  begin
    Result := (Byte(Buf^) - Ord('0')) * 10 + Byte((Buf + 1)^) - Ord('0');
  end;

var
  P: PChar;
begin
  Result := 0;
  try
    if FieldType = ftDate then
      if Length(S) >= 8 then
      begin
        P := PChar(S);
        Result := EncodeDate(StrToInt(Copy(S, 1, 4)), GetNumber(P + 4), GetNumber(P + 6));
      end
      else
    else
      if Length(S) >= 6 then
      begin
        P := PChar(S);
        Result := EncodeTime(GetNumber(P), GetNumber(P + 2), GetNumber(P + 4), 0);
      end;
  except
  end;
end;

function FormatTime(const S: string): string;
var
  DT: TDateTime;
begin
  if EIsZero(S) then Result := ''
  else
  begin
    DT := ConvertStrToDateTime(S, ftTime);
    if DT <> 0 then
      Result := TimeToStr(DT)
    else
      Result := SInvalidTime;
  end;
end;

function FormatDate(const S: string): string;
var
  DT: TDateTime;
begin
  if EIsZero(S) then Result := ''
  else
  begin
    DT := ConvertStrToDateTime(S, ftDate);
    if DT <> 0 then
      Result := DateToStr(DT)
    else
      Result := SInvalidDate;
  end;
end;

function FormatFloatPoint(const S: string): string;
var
  i, Start: Integer;
begin
  Result := S;
  if Result <> '' then begin
    i := 1;
    if CharInSet(Result[i], ['+', '-']) then Inc(i);
    Start := i;
    while (i <= Length(S)) and (Result[i] = '0') do Inc(i);
    if i > Start then begin
      if (i > Length(S)) or (Result[i] = '.') then Dec(i);
      Delete(Result, Start, i - Start);
    end;

    i := Pos('.', Result);
    if i > 0 then
      Result[i] := {$if CompilerVersion>22.0}FormatSettings.{$ifend}DecimalSeparator;
  end;
end;

function TTableForm.SelectedRow: Integer;
begin
  Result := ListView_GetNextItem(LV.Handle, -1, LVNI_ALL or LVNI_SELECTED);
end;

function TTableForm.FormatValue(const Value: string; Fld: TMicexField; RowIndex: Integer;
  JustIds, ThousandSeparators: Boolean): string;
var
  Index: Integer;
  Decimals: Integer;
begin
  Result := Value;
  if Assigned(Fld.EnumPtr) then
    with Fld.EnumPtr do begin
      case IdViewMode of
        ivmNames: Result := GetLongName(Result);
        ivmAll: FmtStr(Result, '%s (%s)', [Result, GetLongName(Result)]);
      end;
    end
  else
    case Fld.FieldType of
      ftChar:
        begin
          if (IdViewMode <> ivmIds) and not JustIds then
          begin
            case Fld.NameType of
              fntUserId:
                if FConnect.FUserList.Find(Value, Index) then
                  with FConnect.FUserList[Index]^ do
                    FmtStr(Result, '%s, %s', [UserName, FirmName]);
              fntFirmId:
                if FConnect.FFirmList.Find(Value, Index) then
                  Result := FConnect.FFirmList[Index].ShortName;
              fntFirmUserId:
                if FConnect.FUserList.Find(Value, Index) then
                  with FConnect.FUserList[Index]^ do
                    FmtStr(Result, '%s, %s', [UserName, FirmName])
                else
                  if FConnect.FFirmList.Find(Value, Index) then
                    with FConnect.FFirmList[Index]^ do
                      Result := ShortName;
              fntSecCode:
                if FConnect.FSecCodeList.Find(Value, Index) then
                  Result := FConnect.FSecCodeList[Index].ShortName;
              fntSecBoard:
                if FConnect.FBoardList.Find(Value, Index) then
                  Result := FConnect.FBoardList[Index].ShortName;
              else
                Result := Trim(Value);  // FNameType = fntNone
            end;
            if (IdViewMode = ivmAll) and (Fld.NameType <> fntNone) then FmtStr(Result, '%s (%s)', [Trim(Value), Result]);
          end
          else
            Result := Trim(Value);  // IdViewMode = ivmIds
        end;
      ftTime: Result := FormatTime(Trim(Value));
      ftDate: Result := FormatDate(Trim(Value));
      ftFixed:
        begin
          Result := EFormatNumber(Value, Fld.Decimals);
          if ThousandSeparators then Result := EDisplayInt(Result);
        end;
      ftInteger:
        begin
          Result := EFormatNumber(Value, 0);
          if ThousandSeparators then Result := EDisplayInt(Result);
        end;
      ftFloat:
        begin
          Decimals := FRows.RowDecimals(RowIndex);
          Result := EFormatNumber(Value, Decimals);
          if ThousandSeparators then Result := EDisplayInt(Result);
          if Decimals < 0 then Result := '?' + Result;  // непонятно какой DECIMALS
        end;
      ftFloatPoint: Result := FormatFloatPoint(Value);
    end;
  if not (FMsg.IsOrderbook and (Fld.Name = 'PRICE')) and
    ((Fld.FieldType <> ftChar) and EIsZero(Result)) then Result := ''
end;

procedure TTableForm.LVData(Sender: TObject; Item: TListItem);
var
  I, RowIndex, FldIndex: Integer;
begin
  if Assigned(FFilterField) or Assigned(FSortField) then
    if (Item.Index < 0) or (Item.Index >= FMapping.Count) then Exit
    else RowIndex := FMapping[Item.Index]
  else RowIndex := Item.Index;

  with Item do
    if (RowIndex >= 0) and (RowIndex < FRows.Count) then
  begin
    if SelectedRow = RowIndex then Caption := '>' else Caption := '';

    for I := 1 to LV.Columns.Count - 1 do
    begin
      FldIndex := I - 1; //LV.Columns[I].Tag;
      with FMsg.OutFields do
        SubItems.Add(FormatValue(FRows[RowIndex, FldIndex], Fields[FldIndex], RowIndex, False, True));
    end;
  end;
end;

function GetTsmrStr(const Err: Integer; const Msg: PMTEMsg): string;
begin
  if ((Err = MTE_TSMR) or (Err = MTE_TRANSREJECTED) or (Err = MTE_NOTIMPLEMENTED) or
    (Err = MTE_TEUNAVAIL) or (Err = MTE_FATALERROR)) and
      Assigned(Msg) and (Msg.DataLen > 0) and (Msg.DataLen < 4096) then
        SetString(Result, PAnsiChar(@Msg.Data), Msg.DataLen)
  else
    Result := '';
end;

procedure ShowError(const AConnect: TMTEConnect; const Err: Integer; const ErrMsg, ErrSource: string);
var
  S: string;
  MsgCaption, Language: string;
begin
  MsgCaption := '';
  Language := '';
  if Assigned(AConnect) then
  begin
    MsgCaption := AConnect.FCaption;
    Language := AConnect.fLanguage;
  end;
  if MsgCaption = '' then MsgCaption := SMsgDlgError;
  FmtStr(S, 'Код %d: %s', [Err, MTEErrorMsgEx(Err, PAnsiChar(AnsiString(Language)))]);
  case Err of
    MTE_OK:
      Application.MessageBox(PChar(S + ^M^M + ErrMsg), PChar(MsgCaption), MB_ICONINFORMATION or MB_OK or MB_SETFOREGROUND);
    MTE_TSMR, MTE_TRANSREJECTED:
      Application.MessageBox(PChar(S + ^M^M + ErrMsg), PChar(MsgCaption), MB_ICONHAND or MB_OK or MB_SETFOREGROUND);
    else Application.MessageBox(PChar(S + ' [' + ErrSource + ']' + ^M^M + ErrMsg), PChar(MsgCaption), MB_ICONHAND or MB_OK or MB_SETFOREGROUND);
  end
end;

procedure ShowTransExResult(AConnect: TMTEConnect; Err: Integer; Reply: TMTEExecTransResult; ErrSource: string);

  function PrintParams(const R: TMTETransReply): string;
  var
    i: Integer;
  begin
    Result := '';
    if R.ParamCount > 0 then begin
      for i := 0 to R.ParamCount - 1 do
        Result := Result + Format(sLineBreak + #9'Param#%d'#9'Name: %s'#9'Value: %s', [i, R.Params[i].Name, R.Params[i].Value]);
    end;
  end;

var
  i: Integer;
  S: string;
begin
  case Reply.ReplyCount of
    0: S := '';
    1: S := string(Reply.Replies^[0].MsgText) + PrintParams(Reply.Replies^[0]);
    else begin
      FmtStr(S, '%d replies returned:', [Reply.ReplyCount]);
      for i := 0 to Pred(Reply.ReplyCount) do
        with Reply.Replies^[i] do begin
        S := S + Format(sLineBreak + '%d. Result=%d, MsgCode=%d, MsgText="%s", Params=%d',
          [i, ErrCode, MsgCode, MsgText, ParamCount]) + PrintParams(Reply.Replies^[i]);
      end;
    end;
  end;
  ShowError(AConnect, Err, S, ErrSource);
end;

function FindSecField(Secs: TMicexMessage; const FieldName: string;
  AlwaysFind: Boolean = True): Integer;
begin
  if not Secs.OutFields.Find(FieldName, Result) then
   if AlwaysFind then
      raise Exception.CreateFmt('Can''t find mandatory field %s in %s table',
        [FieldName, Secs.Name])
   else Result := -1;
end;

procedure FetchTable(const Data: PMTEMsg; const Rows: TRowList);
var
  Msg: PMTETable;
  P: PByte;
begin
  Msg := @Data.Data;
  if Msg.RowCount > 0 then
  begin
    P := @Msg.TblData;
    repeat
      Rows.AddRow(P);
      Dec(Msg.RowCount);
    until Msg.RowCount = 0;
  end;
end;

procedure DownloadSecTypes(Connect: TMTEConnect; Iface: TMicexIface;
  TableIndex: Integer; var FoSecTypes: TStringList; SecList: TSecProps);
var
  Table: TMicexMessage;
  Rows: TRowList;
  I, ErrCode, SecTypeIdx, DecimalsIdx: Integer;
  Data: PMTEMsg;
begin
  Table := Iface.Tables[TableIndex];
  Rows := TRowList.Create(Table, SecList);
  try
    ErrCode := MTEOpenTable(Connect.FIdx, SFoSecTypeTable,
      PAnsiChar(StringOfChar(AnsiChar(#32), Table.InFields.SumSize)), Connect.FInitialOpenMode, Data);
    if ErrCode < MTE_OK then
    begin
      MainForm.ValidateConnect(Connect, ErrCode);
      ShowError(Connect, ErrCode, GetTsmrStr(ErrCode, Data), 'MTEOpenTable(' + SFoSecTypeTable + ')')
    end
    else
    try
      FetchTable(Data, Rows);
      SecTypeIdx := FindSecField(Table, 'FOSECTYPEID');
      DecimalsIdx := FindSecField(Table, 'DECIMALS');

      FoSecTypes := TFastStringList.Create;
      for I := 0 to Rows.Count - 1 do
      begin
        FoSecTypes.AddObject(Rows.Values[I, SecTypeIdx],
          TObject(StrToInt(Rows.Values[I, DecimalsIdx])));
      end;
    finally
      MTECloseTable(Connect.FIdx, ErrCode);
    end;
  finally
    Rows.Free;
  end;
end;

procedure DownloadFirms(Connect: TMTEConnect; const Language: string);
var
  TblIdx, ErrCode, IdIdx, NameIdx, I, FirmIdIdx, FoundIdx: Integer;
  Firms, Users, Boards: TMicexMessage;
  Data: PMTEMsg;
  Rows: TRowList;
  FirmId, FirmName: string;
begin
  Connect.FFirmList.Clear;
  Connect.FUserList.Clear;
  Connect.FBoardList.Clear;
  if Connect.FIface.Tables.Find('FIRMS', TblIdx) and not Connect.FDisconnected then
  begin
    Firms := Connect.FIface.Tables[TblIdx];
    Rows := TRowList.Create(Firms, Connect.FSecList);
    try
      ErrCode := MTEOpenTable(Connect.FIdx, 'FIRMS',
        PAnsiChar(StringOfChar(AnsiChar(#32), Firms.InFields.SumSize)), Connect.FInitialOpenMode, Data);
      if ErrCode < MTE_OK then
      begin
        MainForm.ValidateConnect(Connect, ErrCode);
        ShowError(Connect, ErrCode, GetTsmrStr(ErrCode, Data), 'MTEOpenTable(FIRMS)');
      end
      else
      try
        FetchTable(Data, Rows);
        IdIdx := FindSecField(Firms, 'FIRMID', False);
        if IdIdx < 0 then IdIdx := FindSecField(Firms, 'ID');
        NameIdx := FindSecField(Firms, 'FIRMNAME', False);
        if NameIdx < 0 then NameIdx := FindSecField(Firms, 'NAME');

        for I := 0 to Rows.Count - 1 do
          Connect.FFirmList.Add(Rows.Values[I, IdIdx], Trim(Rows.Values[I, NameIdx]));
      finally
        MTECloseTable(Connect.FIdx, ErrCode);
      end;
    finally
      Rows.Free;
    end;
  end
  else
    MainForm.ShowStatus('Warning: Can''t find FIRMS tables');

  if Connect.FIface.Tables.Find('USERS', TblIdx) and not Connect.FDisconnected then
  begin
    Users := Connect.FIface.Tables[TblIdx];
    Rows := TRowList.Create(Users, Connect.FSecList);
    try
      ErrCode := MTEOpenTable(Connect.FIdx, 'USERS',
        PAnsiChar(StringOfChar(AnsiChar(#32), Users.InFields.SumSize)), Connect.FInitialOpenMode, Data);
      if ErrCode < MTE_OK then
      begin
        MainForm.ValidateConnect(Connect, ErrCode);
        ShowError(Connect, ErrCode, GetTsmrStr(ErrCode, Data), 'MTEOpenTable(USERS)');
      end
      else
      try
        FetchTable(Data, Rows);
        IdIdx := FindSecField(Users, 'USERID', False);
        if IdIdx < 0 then IdIdx := FindSecField(Users, 'ID');
        FirmIdIdx := FindSecField(Users, 'FIRMID', False);
        NameIdx := FindSecField(Users, 'USERNAME', False);
        if NameIdx < 0 then NameIdx := FindSecField(Users, 'NAME');

        for I := 0 to Rows.Count - 1 do
        begin
          if FirmIdIdx >= 0 then FirmId := Rows.Values[I, FirmIdIdx] else FirmId := '';
          if (FirmId <> '') and Connect.FFirmList.Find(FirmId, FoundIdx) then
            FirmName := Connect.FFirmList[FoundIdx].ShortName else FirmName := '';
          Connect.FUserList.Add(Rows.Values[I, IdIdx], Trim(Rows.Values[I, NameIdx]),
            FirmId, FirmName);
        end;
      finally
        MTECloseTable(Connect.FIdx, ErrCode);
      end;
    finally
      Rows.Free;
    end;
  end
  else
    MainForm.ShowStatus('Warning: Can''t find USERS tables');

  if Connect.FIface.Tables.Find('BOARDS', TblIdx) and not Connect.FDisconnected then
  begin
    Boards := Connect.FIface.Tables[TblIdx];
    Rows := TRowList.Create(Boards, Connect.FSecList);
    try
      ErrCode := MTEOpenTable(Connect.FIdx, 'BOARDS',
        PAnsiChar(StringOfChar(AnsiChar(#32), Boards.InFields.SumSize)), Connect.FInitialOpenMode, Data);
      if ErrCode < MTE_OK then
      begin
        MainForm.ValidateConnect(Connect, ErrCode);
        ShowError(Connect, ErrCode, GetTsmrStr(ErrCode, Data), 'MTEOpenTable(BOARDS)');
      end
      else
      try
        FetchTable(Data, Rows);
        IdIdx := FindSecField(Boards, 'BOARDID', False);
        if IdIdx < 0 then IdIdx := FindSecField(Boards, 'ID');
        if Language = 'English' then NameIdx := FindSecField(Boards, 'LATNAME', False) else NameIdx := -1;
        if NameIdx < 0 then NameIdx := FindSecField(Boards, 'BOARDNAME', False);
        if NameIdx < 0 then NameIdx := FindSecField(Boards, 'NAME');

        for I := 0 to Rows.Count - 1 do
          Connect.FBoardList.Add(Rows.Values[I, IdIdx], Trim(Rows.Values[I, NameIdx]));
      finally
        MTECloseTable(Connect.FIdx, ErrCode);
      end;
    finally
      Rows.Free;
    end;
  end
  else
    MainForm.ShowStatus('Warning: Can''t find BOARDS tables');
end;

procedure DownloadSecurities(Connect: TMTEConnect; const Language: string);
var
  TblIdx, ErrCode, I, BoardIdx, SecCodeIdx, ShortNameIdx,
    DecimalsIdx, SecTypeIdx, Index: Integer;
  Rows: TRowList;
  Secs: TMicexMessage;
  Data: PMTEMsg;
  FoSecTypes: TStringList;
  SecCode, ShortName, SecTypeId, SecUniq: string;
begin
  // is it FO engine? if so DECIMALS must be taken from FOSECTYPES table
  Connect.FSecList.Clear;
  Connect.FSecCodeList.Clear;
  FoSecTypes := nil;
  try
    if Connect.FIface.Tables.Find(SFoSecTypeTable, TblIdx) and not Connect.FDisconnected then
      DownloadSecTypes(Connect, Connect.FIface, TblIdx, FoSecTypes, Connect.FSecList);

    if Connect.FIface.Tables.Find(SSecTable, TblIdx) and not Connect.FDisconnected then
    begin
      Secs := Connect.FIface.Tables[TblIdx];
      Rows := TRowList.Create(Secs, Connect.FSecList);
      try
        ErrCode := MTEOpenTable(Connect.FIdx, SSecTable,
          PAnsiChar(StringOfChar(AnsiChar(#32), Secs.InFields.SumSize)), Connect.FInitialOpenMode, Data);
        if ErrCode < MTE_OK then
        begin
          MainForm.ValidateConnect(Connect, ErrCode);
          ShowError(Connect, ErrCode, GetTsmrStr(ErrCode, Data), 'MTEOpenTable(' + SSecTable + ')');
        end
        else
        try
          FetchTable(Data, Rows);
          BoardIdx := FindSecField(Secs, 'SECBOARD');
          SecCodeIdx := FindSecField(Secs, 'SECCODE');
          if Language = 'English' then ShortNameIdx := FindSecField(Secs, 'LATNAME', False) else ShortNameIdx := -1;
          if ShortNameIdx < 0 then ShortNameIdx := FindSecField(Secs, 'SHORTNAME', False);
          if ShortNameIdx < 0 then ShortNameIdx := FindSecField(Secs, 'SECNAME');
          DecimalsIdx := FindSecField(Secs, 'DECIMALS', False);
          if DecimalsIdx < 0 then SecTypeIdx := FindSecField(Secs, 'FOSECTYPEID', False)
            else SecTypeIdx := -1;

          for I := 0 to Rows.Count - 1 do
          begin
            SecUniq := Rows.Values[I, SecCodeIdx];
            SecCode := Rows.Values[I, BoardIdx] + SecUniq;
            ShortName := Trim(Rows.Values[I, ShortNameIdx]);

            if DecimalsIdx < 0 then
            begin
              if SecTypeIdx >= 0 then begin
                SecTypeId := Rows.Values[I, SecTypeIdx];
                if FoSecTypes.Find(SecTypeId, Index) then
                  Connect.FSecList.Add(SecCode, ShortName,
                    Integer(FoSecTypes.Objects[Index]))
                else
                  raise Exception.CreateFmt('Invalid FO security type: ''%s''',
                    [SecTypeId]);
              end else
                Connect.FSecList.Add(SecCode, ShortName, -1);
            end
            else
              Connect.FSecList.Add(SecCode, ShortName,
                StrToInt(Rows.Values[I, DecimalsIdx]));

            if not Connect.FSecCodeList.Find(SecUniq, Index) then
              Connect.FSecCodeList.Add(SecUniq, ShortName);
          end;
        finally
          MTECloseTable(Connect.FIdx, ErrCode);
        end;
      finally
        Rows.Free;
      end;
    end
    else
//      raise Exception.CreateFmt('Can''t find %s tables', [SSecTable]);
      MainForm.ShowStatus(Format('Warning: Can''t find %s tables', [SSecTable]));
  finally
    FoSecTypes.Free;
  end;
end;

{ TSecPropList }

procedure TSecProps.Add(const ASecCode, AShortName: string; ADecimals: Integer);
var
  P: PSecProp;
begin
  New(P);
  try
    P.SecCode := ASecCode;
    P.Decimals := ADecimals;
    P.ShortName := Trim(AShortName);
    if P.ShortName = '' then P.ShortName := ASecCode;
    inherited AddObject(ASecCode, TObject(P));
  except
    Dispose(P);
    raise;
  end;
end;

procedure TSecProps.Clear;
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
    Dispose(PSecProp(Objects[I]));
  inherited Clear;
end;

destructor TSecProps.Destroy;
begin
  Clear;
  inherited;
end;

function TSecProps.GetProps(AIndex: Integer): PSecProp;
begin
  Result := PSecProp(Objects[AIndex]);
end;

procedure TTableForm.ExecuteLink(Sender: TObject);
var
  Msg: TMicexMessage;
  FromMsg: TBaseMsgRec;
begin
  Msg := TMicexMessage((Sender as TMenuItem).Tag);
  if Assigned(Msg) then
  begin
    FromMsg.Msg := FMsg;
    FromMsg.Rows := FRows;
    FromMsg.RowNum := LV.Selected.Index;
    if Assigned(FFilterField) or Assigned(FSortField) then
      FromMsg.RowNum := FMapping[FromMsg.RowNum];
    with MainForm do
      if Msg.IsTable then OpenTable(ConnectPtr, Msg, @FromMsg)
      else ExecuteTransaction(ConnectPtr, Msg, @FromMsg);
  end;
end;

procedure TTableForm.pmLinkPopup(Sender: TObject);

  function HasCommonFields(InMsg, OutMsg: TMicexMessage): Boolean;
  var
    I, Dummy: Integer;
  begin
    Result := True;
    for I := 0 to InMsg.InFields.Count - 1 do
      if OutMsg.OutFields.Find(InMsg.InFields[I].Name, Dummy) then
        Exit;
    Result := False;
  end;

  procedure AddMenuItem(const ACaption: string; Msg: TMicexMessage);
  var
    Item: TMenuItem;
  begin
    Item := TMenuItem.Create(pmLink);
    with Item do
    begin
      Caption := ACaption;
      Tag := IntPtr(Msg);
      OnClick := ExecuteLink;
    end;
    pmLink.Items.Add(Item);
  end;

  procedure AddMessages(Messages: TMicexMessages);
  var
    Msg: TMicexMessage;
    I: Integer;
  begin
    for I := 0 to Messages.Count - 1 do
    begin
      Msg := Messages[I];
      if HasCommonFields(Msg, FMsg) then
        AddMenuItem(Msg.Name + ' - ' + Msg.Caption, Msg);
    end;
  end;

var
  I: Integer;
begin
  inherited;
  for I := pmLink.Items.Count - 1 downto 0 do
    if pmLink.Items[I].Tag <> 0 then
      pmLink.Items.Delete(I);

  miDelFilter.Enabled := Assigned(FFilterField);
  if not Assigned(LV.Selected) then Exit;

  AddMessages(FConnect.FIface.Tables);
  I := pmLink.Items.Count;
  if I > 0 then
  begin
    AddMenuItem('-', nil);
    Inc(I);
  end;
  AddMessages(FConnect.FIface.Transactions);
  with pmLink.Items do
    if (Count > 0) and (Count = I) then Delete(Count - 1);
end;

{ TMTEConnect }

constructor TMTEConnect.Create;
begin
  inherited Create;
  FSecList := TSecProps.Create;
  FSecCodeList := TFirmProps.Create;
  FFirmList := TFirmProps.Create;
  FUserList := TUserProps.Create;
  FBoardList := TFirmProps.Create;
  FParams := TStringList.Create;
  FInitialOpenMode := True;
end;

destructor TMTEConnect.Destroy;
var
  TmpHandle, Err: Integer;
begin
  if FIdx >= 0 then
  begin
    // close "keep-alive" (systime) table
    TmpHandle := FSysTimeTable;
    FSysTimeTable := -1;
    if TmpHandle >= MTE_OK then MTECloseTable(FIdx, TmpHandle);

    MainForm.CutTrees(Self);
    Err := MTEDisconnect(FIdx);
    if (Err <> MTE_OK) and not FDisconnected then ShowError(Self, Err, '', 'MTEDisconnect');
    FIface.Free;
  end;

  FParams.Free;
  FSecList.Free;
  FFirmList.Free;
  FUserList.Free;
  FSecCodeList.Free;
  FBoardList.Free;
  inherited Destroy;
end;

function TMTEConnect.GetErrorMsg(const Err: Integer): string;
begin
  Result := string(MTEErrorMsgEx(Err, PAnsiChar(AnsiString(fLanguage))));
end;

function TTableForm.ConnectIdx: Integer;
begin
  Result := FConnect.FIdx;
end;

procedure TTableForm.DoFilter;
var
  I, NewCount: Integer;
  Value: string;
  DoScroll: Boolean;
begin
  FMapping.Clear;
  if Assigned(FFilterField) then
  begin
    case FFilterIndex of
      0:  // filter by specified value
        begin
          Value := AnsiUpperCase(FFilterValue);
          for I := 0 to FRows.Count - 1 do
            if Pos(Value, AnsiUpperCase(FRows[I, FFilterFieldIndex])) > 0 then
              FMapping.Add(I);
        end;
      1:  // show only not empty values
        for I := 0 to FRows.Count - 1 do
        begin
          Value := Trim(FRows[I, FFilterFieldIndex]);
          if (Value <> '') and not EIsZero(Value) then FMapping.Add(I);
        end;
      2:  // show only empty values
        for I := 0 to FRows.Count - 1 do
        begin
          Value := Trim(FRows[I, FFilterFieldIndex]);
          if (Value = '') or EIsZero(FRows[I, FFilterFieldIndex]) then FMapping.Add(I);
        end;
    end;
  end
  else
    if Assigned(FSortField) then
      for I := 0 to FRows.Count - 1 do
        FMapping.Add(I);

  if Assigned(FSortField) then FMapping.Sort;

  with LV, Items do
  begin
    DoScroll := (Count = 0) or (SelectedRow = Count - 1);
    if Assigned(FFilterField) or Assigned(FSortField) then NewCount := FMapping.Count
    else NewCount := FRows.Count;
    if Count <> NewCount then
    begin
      Count := NewCount;
      if DoScroll then SelectLastRec;
    end;
    Invalidate;
  end;
  UpdateCaption;
end;

procedure TTableForm.UpdateCaption;

  function SystemName: string;
  begin
    if (SystemIndex <> 0) then
      Result := Format(' (sysidx=%d)', [SystemIndex])
    else
      Result := '';
  end;

begin
  Caption := Format('%s%s - %d/%d - %s', [FCaption, SystemName, LV.Items.Count, FRows.Count, FConnectCaption]);
end;

procedure TTableForm.miFilterClick(Sender: TObject);
var
  P: TPoint;
  I, W, MouseX: Integer;
  FilterFieldCaption: string;
begin
  inherited;
  if Assigned(FFilterField) then
    FilterFieldCaption := GetDisplayName(FFilterField, True)
  else begin
    FilterFieldCaption := '';
    if (Sender is TMenuItem) and (TMenuItem(Sender).GetParentMenu is TPopupMenu) then
      P := TPopupMenu(TMenuItem(Sender).GetParentMenu).PopupPoint
    else
      P := Mouse.CursorPos;
    MouseX := LV.ScreenToClient(P).X + LV.ViewOrigin.X;
    W := LV.Columns[0].Width;
    for I := 1 to LV.Columns.Count - 1 do
    begin
      Inc(W, LV.Columns[I].Width);
      if MouseX < W then
      begin
        FilterFieldCaption := GetDisplayName(FMsg.OutFields[I - 1], True);
        Break;
      end;
    end;
  end;

  with TFilterForm.Create(Self) do
  try
    with cbFields do
    begin
      for I := 0 to FMsg.OutFields.Count - 1 do
        Items.Add(GetDisplayName(FMsg.OutFields[I], True));
      ItemIndex := Items.IndexOf(FilterFieldCaption);
      if ItemIndex < 0 then ItemIndex := 0;
    end;
    BaseTable := FMsg;
    ShowPossibleFieldValues;
    SetFilterValue(FFilterValue);
    rbValue.Checked := FFilterIndex = 0;
    rbNotEmpty.Checked := FFilterIndex = 1;
    rbEmpty.Checked := FFilterIndex = 2;

    if ShowModal = mrOk then
    begin
      if rbValue.Checked then FFilterIndex := 0
      else
        if rbNotEmpty.Checked then FFilterIndex := 1
        else
          if rbEmpty.Checked then FFilterIndex := 2
          else FFilterIndex := -1;
      FFilterFieldIndex := cbFields.ItemIndex;
      FFilterField := FMsg.OutFields[FFilterFieldIndex];
      FFilterValue := GetFilterValue;
      DoFilter;
    end;
  finally
    Free;
  end;
end;

procedure TTableForm.miDelFilterClick(Sender: TObject);
begin
  inherited;
  FFilterField := nil;
  FFilterFieldIndex := -1;
  DoFilter;
end;

procedure TTableForm.LVDblClick(Sender: TObject);
begin
  inherited;
  miFilterClick(LV);
end;

procedure TTableForm.LVColumnClick(Sender: TObject; Column: TListColumn);
var
  NewSortField: TMicexField;
begin
  inherited;
  if Assigned(FSortColumn) then
    FSortColumn.Caption := GetDisplayName(FMsg.OutFields[FSortColumn.Tag], True);

  if (Column.Tag >= 0) and (Column.Tag < FMsg.OutFields.Count) then
  begin
    NewSortField := FMsg.OutFields[Column.Tag];
    if NewSortField = FSortField then
      if FSortDescending then
        FSortField := nil // sequence is "No sort" -> "Ascending" -> "Descending"
      else
        FSortDescending := True
    else
    begin
      FSortDescending := False;
      FSortField := NewSortField;
      FSortFieldIndex := Column.Tag;
    end;
    FSortColumn := Column;

    if Assigned(FSortColumn) and Assigned(FSortField) then
      if FSortDescending then
        FSortColumn.Caption := '^ ' + GetDisplayName(FMsg.OutFields[FSortColumn.Tag], True)
      else
        FSortColumn.Caption := 'V ' + GetDisplayName(FMsg.OutFields[FSortColumn.Tag], True);

    DoFilter;
  end;
end;

procedure TMTEConnect.SetupKeepAlive;
const
  SystimeTables: array [0..2] of string = ('SYSTIME', 'TESYSTIME', 'RESYSTIME');
var
  TableName: string;
  SysTimeIdx: Integer;
  Data: PMTEMsg;
begin
  for TableName in SystimeTables do
    if FIface.Tables.Find(TableName, SysTimeIdx) then begin
      FSysTimeTable := MTEOpenTable(FIdx, PAnsiChar(AnsiString(TableName)), '', True, Data);
      Break;
    end;
end;

{ TFirmProps }

procedure TFirmProps.Add(const AFirmId, AFirmName: string);
var
  P: PFirmProp;
begin
  New(P);
  P.Code := AFirmId;
  P.ShortName := AFirmName;
  inherited AddObject(AFirmId, TObject(P));
end;

procedure TFirmProps.Clear;
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
    Dispose(Props[I]);
  inherited;
end;

destructor TFirmProps.Destroy;
begin
  Clear;
  inherited;
end;

function TFirmProps.GetProps(AIndex: Integer): PFirmProp;
begin
  Result := PFirmProp(Objects[AIndex]);
end;

{ TUserProps }

procedure TUserProps.Add(const AUserId, AUserName, AFirmId, AFirmName: string);
var
  P: PUserProp;
begin
  New(P);
  P.UserName := AUserName;
  P.FirmId := AFirmId;
  P.FirmName := AFirmName;
  inherited AddObject(AUserId, TObject(P));
end;

procedure TUserProps.Clear;
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
    Dispose(Props[I]);
  inherited;
end;

destructor TUserProps.Destroy;
begin
  Clear;
  inherited;
end;

function TUserProps.GetProps(AIndex: Integer): PUserProp;
begin
  Result := PUserProp(Objects[AIndex]);
end;

procedure TTableForm.miExportClick(Sender: TObject);
var
  RowCount, RowIndex, ColIndex, FldIndex, RealRow: Integer;
  T: TextFile;
  S, Value: string;
  Separator: Char;
  RawFormat: Boolean;
begin
  MainForm.ExportDlg.FileName := FMsg.Name;
  if MainForm.ExportDlg.Execute then
  begin
    if Assigned(FFilterField) or Assigned(FSortField) then RowCount := FMapping.Count
    else RowCount := FRows.Count;

    AssignFile(T, MainForm.ExportDlg.FileName);
    Rewrite(T);
    try
      case MainForm.ExportDlg.FilterIndex of
        1: Separator := FormatSettings.ListSeparator;
        else Separator := #9;
      end;
      RawFormat := Separator = #9;

      // save column headers
      S := '';
      for ColIndex := 1 to LV.Columns.Count - 1 do
      begin
        if RawFormat then
          FldIndex := ColIndex - 1
        else
          FldIndex := LV.Columns[ColIndex].Tag;
        if S <> '' then S := S + Separator;
        with FMsg.OutFields do begin
          if RawFormat then
            Value := Fields[FldIndex].Name
          else
            Value := GetDisplayName(Fields[FldIndex], True);
          if Value.IndexOfAny([Separator, '"']) >= 0 then
            Value := AnsiQuotedStr(Value, '"');
          S := S + Value;
        end;
      end;
      Writeln(T, S);

      // save table rows
      for RowIndex := 0 to RowCount - 1 do
      begin
        if Assigned(FFilterField) or Assigned(FSortField) then RealRow := FMapping[RowIndex]
        else RealRow := RowIndex;

        S := '';
        for ColIndex := 1 to LV.Columns.Count - 1 do
        begin
          if RawFormat then
            FldIndex := ColIndex - 1
          else
            FldIndex := LV.Columns[ColIndex].Tag;
          if S <> '' then S := S + Separator;
          with FMsg.OutFields do begin
            Value := FRows[RealRow, FldIndex];
            if RawFormat then
              Value := Trim(Value)
            else
              Value := FormatValue(Value, Fields[FldIndex], RowIndex, False, False);
            if Value.IndexOfAny([Separator, '"']) >= 0 then
              Value := AnsiQuotedStr(Value, '"');
            S := S + Value;
          end;
        end;
        Writeln(T, S);
      end;
    finally
      CloseFile(T);
    end;
  end;
end;

function TTableForm.SystemIndex: Integer;
begin
  Result := fSystemIndex;
end;

{ TMappingComparer }

constructor TMappingComparer.Create(const aTableForm: TTableForm);
begin
  inherited Create;
  fTableForm := aTableForm;
end;

function TMappingComparer.Compare(const aLeft, aRight: Integer): Integer;
var
  Value1, Value2: string;
  Decimals1, Decimals2: Integer;
  F: Extended;
begin
  with fTableForm do
  begin
    Value1 := FRows.Values[aLeft, FSortFieldIndex];
    Decimals1 := FRows.RowDecimals(aLeft);
    Value2 := FRows.Values[aRight, FSortFieldIndex];
    Decimals2 := FRows.RowDecimals(aRight);

    case FSortField.FieldType of
      ftInteger:
        Result := StrToIntDef(Value1, 0) - StrToIntDef(Value2, 0);

      ftChar, ftDate, ftTime:
        Result := CompareStr(Value1, Value2);

      ftFloat, ftFixed, ftFloatPoint:
        begin
          if FSortField.FieldType = ftFixed then
            F := StrToFloatDef(EFormatNumber(Value1, 2), 0) -
              StrToFloatDef(EFormatNumber(Value2, 2), 0)
          else if FSortField.FieldType = ftFloat then
            F := StrToFloatDef(EFormatNumber(Value1, Decimals1), 0) -
              StrToFloatDef(EFormatNumber(Value2, Decimals2), 0)
          else  // ftFloatPrice
            F := StrToFloatDef(FormatFloatPoint(Value1), 0) -
              StrToFloatDef(FormatFloatPoint(Value2), 0);
          if F < 0 then Result := -1 else
            if F > 0 then Result := 1 else
              Result := 0;
        end;

      else Result := 0;
    end;
    if Result = 0 then begin
      if aLeft < aRight then Result := -1
      else if aLeft > aRight then Result := 1;
    end;
    if FSortDescending then Result := -Result;
  end;
end;

initialization

  for iSeqNum := Low(FieldSeqNums) to High(FieldSeqNums) do
    FieldSeqNums[iSeqNum] := AnsiChar(iSeqNum);
  {$IFDEF CLIENTLOG}
  AssignFile(DebugFile, 'ASTSClient.Log');
  Rewrite(DebugFile);
  {$ENDIF}

finalization

  {$IFDEF CLIENTLOG}
  CloseFile(DebugFile);
  {$ENDIF}

end.
