{
    Double Commander
    -------------------------------------------------------------------------
    Show file in the text, bin, hex or dec mode

    Copyright (C) 2004 Radek Cervinka (radek.cervinka@centrum.cz)
    Copyright (C) 2006-2026 Alexander Koblov (alexx2000@mail.ru)

    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, see <http://www.gnu.org/licenses/>.
}

(*
   TODO:
   a) File mapping blocks writing into file by other processes.
      Either:
      + Open small text files by reading them all into memory (done).
      - Add optional custom loading/caching portions of file in memory
        and only reading from file when neccessary.

   b) Selecting text does not work well with composed Unicode characters
      (characters that are composed of multiple Unicode characters).

   c) Drawing/selecting text does not work correctly with RTL (right to left) text.

   d) FTextHeight is unreliable with complex unicode characters. It should be
      calculated based on currently displayed text (get max from each line's height).
*)

unit ViewerControl;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Classes, Controls, StdCtrls, LCLVersion, LCLType, LMessages, fgl;

const
  MaxMemSize = $400000; // 4 Mb

type
  TViewerControlMode = (vcmBin, vcmHex, vcmText, vcmWrap, vcmBook, vcmDec);
  TDataAccess = (dtMmap, dtNothing);
  TCharSide = (csBefore, csLeft, csRight, csAfter);

  TPtrIntList = specialize TFPGList<PtrInt>;

  TGuessEncodingEvent = function(const s: string): string;
  TFileOpenEvent = function(const FileName: String; Mode: LongWord): System.THandle;

  TCustomCharsPresentation = class;
  TCharToCustomValueTransformProc = function(AChar:AnsiChar;AMaxDigitsCount:integer):AnsiString of object;

  { TCustomCharsPresentation }

  {
    Presentation one char is called Value
    Function for convert char to Value is ChrToValueProc
  }

  TCustomCharsPresentation = class
  public
    ValuesPerLine  :integer;  // = 16 for Hex by default
    MaxValueDigits :integer;  // the max width of present  char (255) - 3 symbols

    MaxAddrDigits  :integer;  // = 8;
    StartOfs       :integer;  // = OffsetWidth + 2;  // ': '
    EndOfs         :integer;  // = StartOfs + (ValuesPerLine * (ValueMaxDigits+SpaceCount));
    StartAscii     :integer;  // = StartOfs + (ValuesPerLine * (ValueMaxDigits+SpaceCount)) + 2;  // '  '

    SpaceCount     :integer;  // = 1 - one spacebar between Values
    SeparatorSpace :AnsiString;  // spacebar * SpaceCount

    SeparatorChar  :AnsiChar; //  '|'
    CountSeperate  :integer;  // insert SeparatorChar after every CountSeperate values

    ChrToValueProc   :TCharToCustomValueTransformProc; // procedure which return presentation of one char

    constructor Create(APresentValuesPerLine,ACharMaxPresentWidth,AOffsetWidth,ACountSeparate:integer;AChrToValueProc:TCharToCustomValueTransformProc);
    destructor  Destroy();override;
  end;

type
  // If additional encodings are added they should be also supported by:
  // - GetNextCharAsAscii
  // - GetPrevCharAsAscii
  // - GetNextCharAsUtf8
  // - ConvertToUTF8
  // - UpdateSelection
  TViewerEncoding = (veAutoDetect,
                     veUtf8,
                     veUtf8bom,
                     veAnsi,
                     veOem,
                     veCp1250,
                     veCp1251,
                     veCp1252,
                     veCp1253,
                     veCp1254,
                     veCp1255,
                     veCp1256,
                     veCp1257,
                     veCp1258,
                     veCp437,
                     veCp850,
                     veCp852,
                     veCp866,
                     veCp874,
                     veCp932,
                     veCp936,
                     veCp949,
                     veCp950,
                     veIso88591,
                     veIso88592,
                     veKoi8r,
                     veKoi8u,
                     veKoi8ru,
                     veUcs2le,
                     veUcs2be,
                     veUtf16le,
                     veUtf16be,
                     veUtf32le,  // = ucs4le
                     veUtf32be); // = ucs4be

  TViewerEncodings = set of TViewerEncoding;

const
  ViewerEncodingsNames: array [TViewerEncoding] of string =
                   ('Auto-detect',
                    'UTF-8',
                    'UTF-8BOM',
                    'ANSI',
                    'OEM',
                    'CP1250',
                    'CP1251',
                    'CP1252',
                    'CP1253',
                    'CP1254',
                    'CP1255',
                    'CP1256',
                    'CP1257',
                    'CP1258',
                    'CP437',
                    'CP850',
                    'CP852',
                    'CP866',
                    'CP874',
                    'CP932',
                    'CP936',
                    'CP949',
                    'CP950',
                    'ISO-8859-1',
                    'ISO-8859-2',
                    'KOI8-R',
                    'KOI8-U',
                    'KOI8-RU',
                    'UCS-2LE',
                    'UCS-2BE',
                    'UTF-16LE',
                    'UTF-16BE',
                    'UTF-32LE',
                    'UTF-32BE');

const
  ViewerEncodingOem: TViewerEncodings = [
    veCp437, veCp850, veCp852, veCp866];

  ViewerEncodingMultiByte: TViewerEncodings = [
    veCp932, veCp936, veCp949, veCp950,
    veUtf8, veUtf8bom, veUcs2le, veUcs2be,
    veUtf16le, veUtf16be, veUtf32le, veUtf32be];

  ViewerEncodingDoubleByte: TViewerEncodings = [
    veUcs2le, veUcs2be, veUtf16le, veUtf16be   ];

type


  { TViewerControl }

  TViewerControl = class(TCustomControl)
  protected
    FEncoding:           TViewerEncoding;
    FViewerControlMode:  TViewerControlMode;
    FFileName:           String;
    FFileHandle:         System.THandle;
    FFileSize:           Int64;
    FMappingHandle:      System.THandle;
    FMappedFile:         Pointer;
    FPosition:           PtrInt;
    FHPosition:          Integer;  // Tab for text during horizontal scroll
    FHLowEnd:            Integer;  // End for HPosition (string with max char)
    FVisibleOffset:      PtrInt;   // Offset in symbols for current line (see IsVisible and MakeVisible)
    FLowLimit:           PtrInt;   // Lowest possible value for Position
    FHighLimit:          PtrInt;   // Position cannot reach this value
    FBOMLength:          Integer;
    FLineList:           TPtrIntList;
    FBlockBeg:           PtrInt;
    FBlockEnd:           PtrInt;
    FCaretPos:           PtrInt;
    FCaretPoint:         TPoint;
    FMouseBlockBeg:      PtrInt;
    FMouseBlockSide:     TCharSide;
    FSelecting:          Boolean;
    FTextWidth:          Integer; // max char count or width in window
    FTextHeight:         Integer; // measured values of font, rec calc at font changed
    FOnPositionChanged:  TNotifyEvent;
    FScrollBarsLock:     Integer;
    FUpdateScrollBarPos: Boolean; // used to block updating of scrollbar
    FScrollBarPosition:  Integer;  // for updating vertical scrollbar based on Position
    FHScrollBarPosition: Integer;  // for updating horizontal scrollbar based on HPosition
    FColCount:           Integer;
    FTabSpaces:          Integer; // tab width in spaces
    FMaxTextWidth:       Integer; // maximum of chars on one line unwrapped text (max 16384)
    FExtraLineSpacing:   Integer;
    FLeftMargin:         Integer;
    FOnGuessEncoding:    TGuessEncodingEvent;
    FOnFileOpen:         TFileOpenEvent;
    FCaretVisible:       Boolean;
    FShowCaret:          Boolean;
    FAutoCopy:           Boolean;
    FLastError:          String;
    FText:               String;

    FHex:TCustomCharsPresentation;
    FDec:TCustomCharsPresentation;
    FCustom:TCustomCharsPresentation;

    function GetPercent: Integer;
    procedure SetPercent(const AValue: Integer);
    procedure SetBlockBegin(const AValue: PtrInt);
    procedure SetBlockEnd(const AValue: PtrInt);
    procedure SetPosition(Value: PtrInt); virtual;
    procedure SetHPosition(Value: Integer);
    procedure SetPosition(Value: PtrInt; Force: Boolean); overload;
    procedure SetHPosition(Value: Integer; Force: Boolean); overload;
    procedure SetEncoding(AEncoding: TViewerEncoding);
    function GetEncodingName: string;
    procedure SetEncodingName(AEncodingName: string);
    procedure SetViewerMode(Value: TViewerControlMode);
    procedure SetColCount(const AValue: Integer);
    procedure SetMaxTextWidth(const AValue: Integer);
    procedure SetTabSpaces(const AValue: Integer);
    procedure SetShowCaret(AValue: Boolean);
    procedure SetCaretPos(AValue: PtrInt);

    {en
       Returns how many lines (given current FTextHeight) will fit into the window.
    }
    function GetClientHeightInLines(Whole: Boolean = True): Integer; inline;

    {en
       Calculates how many lines can be displayed from given position.
       param(FromPosition
              Position from which to check. It should point to a start of a line.)
       @param(LastLineReached
              If it is set to @true when the function returns, then the last
              line of text was reached when scanning.
              This means that there are no more lines to be displayed other than
              the ones scanned from FromPosition. In other words:
                SetPosition(GetStartOfNextLine(FromPosition)) will be one line
                too many and will be scrolled back.)
    }
    function GetLinesTillEnd(FromPosition: PtrInt; out LastLineReached: Boolean): Integer;

    function GetBomLength: Integer;

    function GetOffsetOfMode(const mode: TViewerControlMode): Integer;

    procedure UpdateLimits;

    {en
       @param(iStartPos
              Should point to start of a line.
              It is increased by the amount of parsed data (with line endings).)
       @param(aLimit
              Position which cannot be reached while reading from file.)
       @param(DataLength
              It is length in bytes of parsed data without any line endings.
              iStartPos is moved beyond the line endings though.)
    }
    function CalcTextLineLength(var iStartPos: PtrInt; const aLimit: Int64; out DataLength: PtrInt): Integer;

    function GetStartOfLine(aPosition: PtrInt): PtrInt;
    function GetEndOfLine(aPosition: PtrInt): PtrInt;

    function GetStartOfPrevLine(aPosition: PtrInt): PtrInt;
    function GetStartOfNextLine(aPosition: PtrInt): PtrInt;

    {en
       Changes the value of aPosition to X lines back or forward.
       @param(aPosition
              File position to change.)
       @param(iLines
              Nr of lines to scroll.
              If positive the position is increased by iLines lines,
              if negative the position is decreased by -iLines lines.)
    }
    function ScrollPosition(var aPosition: PtrInt; iLines: Integer): Boolean;

    {en
       Calculates (x,y) cursor position to a position within file.
       @param(x
              Client X coordinate of mouse cursor.)
       @param(y
              Client Y coordinate of mouse cursor.)
       @param(CharSide
              To which side of a character at returned position the (x,y) points to.
              Only valid if returned position is not -1.)
       @returns(Position in file to which (x,y) points to, based on what is
                currently displayed.
                Returns -1 if (x,y) doesn't point to any position (outside of
                the text for example).)
    }
    function XYPos2Adr(x, y: Integer; out CharSide: TCharSide): PtrInt;

    procedure OutText(x, y: Integer; const sText: String; StartPos: PtrInt; DataLength: Integer);
    procedure OutBin(x, y: Integer; const sText: String; StartPos: PtrInt; DataLength: Integer);

    procedure OutCustom(x, y: Integer; const sText: String;StartPos: PtrInt; DataLength: Integer);  // render one line
    function  TransformCustom(var APosition: PtrInt; ALimit: PtrInt; AWithAdditionalData: Boolean = True): String;
    function  TransformCustomBlock(var APosition: PtrInt; DataLength: Integer; ASeparatorsOn, AAlignData: Boolean; out AChars: String): String;

    function HexToValueProc(AChar:AnsiChar;AMaxDigitsCount:integer):AnsiString;
    function DecToValueProc(AChar:AnsiChar;AMaxDigitsCount:integer):AnsiString;

    procedure WriteBin;
    procedure WriteText;
    procedure WriteCustom; virtual;
    function  TransformText(const sText: String; const Xoffset: Integer): String;
    function  TransformBin(var aPosition: PtrInt; aLimit: PtrInt): String;
    function  TransformHex(var aPosition: PtrInt; aLimit: PtrInt): AnsiString;virtual;

    procedure AddLineOffset(const iOffset: PtrInt); inline;

    procedure DrawLastError;

    function MapFile(const sFileName: String): Boolean;
    procedure UnMapFile;
    procedure SetFileName(const sFileName: String);

    procedure UpdateScrollbars;

    procedure DoOnResize; override;

    {en
       Returns next unicode character from the file, depending on Encoding.
       It is a faster version, which does as little conversion as possible,
       but only Ascii values are guaranteed to be valid (0-127).
       Other unicode values may/may not be valid, so shouldn't be tested.
       This function is used for reading pure ascii characters such as
       line endings, tabs, white spaces, etc.
    }
    function GetNextCharAsAscii(const iPosition: PtrInt; out CharLenInBytes: Integer): Cardinal;
    function GetPrevCharAsAscii(const iPosition: PtrInt; out CharLenInBytes: Integer): Cardinal;

    {en
       Retrieve next character from the file depending on encoding and
       automatically convert it to UTF-8.
       If CharLenInBytes is greater than 0 but the result is an empty string
       then it's possible there was no appropriate UTF-8 character for the
       next character of the current encoding.
    }
    function GetNextCharAsUtf8(const iPosition: PtrInt; out CharLenInBytes: Integer): String;

    procedure ReReadFile;

    {en
       Searches for an ASCII character.
       @param(aPosition
              Position from where the search starts.)
       @param(aMaxBytes
              How many bytes are available for reading.)
       @param(AsciiChars
              The function searches for any character that this string contains.)
       @param(bFindNotIn
              If @true searches for first character not included in AsciiChars.
              If @false searches for first character included in AsciiChars.)
    }
    function FindAsciiSetForward(aPosition, aMaxBytes: PtrInt;
                                 const AsciiChars: String;
                                 bFindNotIn: Boolean): PtrInt;

    {en
       Same as FindForward but it searches backwards from pAdr.
       aMaxBytes must be number of available bytes for reading backwards from pAdr.
    }
    function FindAsciiSetBackward(aPosition, aMaxBytes: PtrInt;
                                  const AsciiChars: String;
                                  bFindNotIn: Boolean): PtrInt;

    {en
       Checks if current selection is still valid given current viewer mode and encoding.
       For example checks if selection is not in the middle of a unicode character.
    }
    procedure UpdateSelectionAndCaret;

    procedure ScrollBarSetPosition(Which, Value: Integer);
    function  ScrollBarGetPosition(Which: Integer): Integer;

    function GetText(const StartPos, Len: PtrInt; const Xoffset: Integer): string;

    procedure SetText(const AValue: String);

  protected
    procedure CreateWnd; override;
    procedure WMVScroll(var Message : TLMVScroll); message LM_VSCROLL;
    procedure WMHScroll(var Message : TLMHScroll); message LM_HSCROLL;
    procedure WMSetFocus(var Message: TLMSetFocus); message LM_SETFOCUS;
    procedure WMKillFocus(var Message: TLMKillFocus); message LM_KILLFOCUS;
    procedure FontChanged(Sender: TObject); override;
    procedure KeyDown(var Key: word; Shift: TShiftState); override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    function DoMouseWheelDown(Shift: TShiftState; MousePos: TPoint): Boolean; override;
    function DoMouseWheelUp(Shift: TShiftState; MousePos: TPoint): Boolean; override;
    function DoMouseWheelLeft(Shift: TShiftState; MousePos: TPoint): Boolean; override;
    function DoMouseWheelRight(Shift: TShiftState; MousePos: TPoint): Boolean; override;
    procedure DoShowCaret;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    procedure Paint; override;

    {en
       Scrolls the displayed text in the window.

       @param(iLines
              Nr of lines to scroll.
              If positive the text is scrolled downwards,
              if negative the text is scrolled upwards.)
       @returns(@true if the text was scrolled.)
    }
    function Scroll(iLines: Integer): Boolean;
    function HScroll(iSymbols: Integer): Boolean;

    procedure PageUp;
    procedure PageDown;
    procedure GoHome;
    procedure GoEnd;

    procedure HPageUp;
    procedure HPageDown;
    procedure HGoHome;
    procedure HGoEnd;

    procedure CaretGoHome;
    procedure CaretGoEnd;

    function GetDataAdr: Pointer;

    procedure SelectAll;
    procedure SelectText(AStart, AEnd: PtrInt);
    procedure CopyToClipboard;
    procedure CopyToClipboardF;
    function Selection: String;

    function IsVisible(const aPosition: PtrInt): Boolean; overload;
    procedure MakeVisible(const aPosition: PtrInt);

    function ConvertToUTF8(const sText: AnsiString): String;
    function ConvertFromUTF8(const sText: String): AnsiString;
    function FindUtf8Text(iStartPos: PtrInt; const sSearchText: String;
                          bCaseSensitive: Boolean; bSearchBackwards: Boolean): PtrInt;

    procedure ResetEncoding;
    function IsFileOpen: Boolean; inline;
    function DetectEncoding: TViewerEncoding;
    procedure GetSupportedEncodings(List: TStrings);

    property Text: String read FText write SetText;
    property Percent: Integer Read GetPercent Write SetPercent;
    property Position: PtrInt Read FPosition Write SetPosition;
    property FileSize: Int64 Read FFileSize;
    property FileHandle: System.THandle read FFileHandle;
    property CaretPos: PtrInt Read FCaretPos Write SetCaretPos;
    property SelectionStart: PtrInt Read FBlockBeg Write SetBlockBegin;
    property SelectionEnd: PtrInt Read FBlockEnd Write SetBlockEnd;
    property EncodingName: string Read GetEncodingName Write SetEncodingName;
    property ColCount: Integer Read FColCount Write SetColCount;
    property MaxTextWidth: Integer read FMaxTextWidth write SetMaxTextWidth;
    property TabSpaces: Integer read FTabSpaces write SetTabSpaces;
    property LeftMargin: Integer read FLeftMargin write FLeftMargin;
    property ExtraLineSpacing: Integer read FExtraLineSpacing write FExtraLineSpacing;
    property AutoCopy: Boolean read FAutoCopy write FAutoCopy;
    property OnGuessEncoding: TGuessEncodingEvent Read FOnGuessEncoding Write FOnGuessEncoding;
    property OnFileOpen: TFileOpenEvent read FOnFileOpen write FOnFileOpen;

  published
    property Mode: TViewerControlMode Read FViewerControlMode Write SetViewerMode default vcmWrap;
    property FileName: String Read FFileName Write SetFileName;
    property Encoding: TViewerEncoding Read FEncoding Write SetEncoding default veAutoDetect;
    property OnPositionChanged: TNotifyEvent Read FOnPositionChanged Write FOnPositionChanged;
    property ShowCaret: Boolean read FShowCaret write SetShowCaret;

    property OnClick;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;

    property OnMouseWheelUp;
    property OnMouseWheelDown;

    property Align;
    property Color;
    property Cursor default crIBeam;
    property Font;
    property ParentColor default False;
    property TabStop default True;
  end;

procedure Register;

implementation

uses
  Math, Graphics, Forms, LCLProc, Clipbrd, LConvEncoding,
  DCUnicodeUtils, LCLIntf, LazUTF8, DCOSUtils , DCConvertEncoding
  {$IF LCL_FULLVERSION >= 4990000}
  , LazUTF16
  {$ENDIF}
  {$IF DEFINED(UNIX)}
  , BaseUnix, Unix, DCUnix
  {$ELSEIF DEFINED(WINDOWS)}
  , Windows, DCWindows
  {$ENDIF};

const
  cBinWidth       = 80;

  // These strings must be Ascii only.
  sNonCharacter: string = ' !"#$%&''()*+,-./:;<=>?@[\]^`{|}~'#13#10#9;
  sWhiteSpace  : string = ' '#13#10#9#8;

const
  ASCII_TABLE: array[0..31] of String =
  (
    '.', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '◙', '♂', '♀', '♪', '♫', '☼',
    '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '→', '←', '∟', '↔', '▲', '▼'
  );

{ TCustomCharsPresentation }

constructor TCustomCharsPresentation.Create(APresentValuesPerLine,
  ACharMaxPresentWidth, AOffsetWidth, ACountSeparate: integer;AChrToValueProc:TCharToCustomValueTransformProc);
begin
  SpaceCount:=1;                             // count of spacebars between values, =1
  ValuesPerLine := APresentValuesPerLine;    // default for hex: 16 values
  MaxAddrDigits   := AOffsetWidth;             // = 8 , count of symbols for display caret offset
  StartOfs      := AOffsetWidth + 2;         // ': '
  MaxValueDigits    := ACharMaxPresentWidth; // hex char (FF) - 2 symbols,  dec char (255) - 3 symbols
  EndOfs        := StartOfs + (ValuesPerLine * (MaxValueDigits+SpaceCount));      // +1 - take in spacebar
  StartAscii    := StartOfs + (ValuesPerLine * (MaxValueDigits+SpaceCount)) + 2;  // '  '

  SeparatorChar:='|';
  CountSeperate:=ACountSeparate;

  SeparatorSpace:=' ';

  ChrToValueProc:=AChrToValueProc;           // method for convert char to Value
end;

destructor TCustomCharsPresentation.Destroy;
begin
  inherited;
end;

// ----------------------------------------------------------------------------

constructor TViewerControl.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Cursor := crIBeam;
  ParentColor := False;
  DoubleBuffered := True;
  ControlStyle := ControlStyle + [csTripleClicks, csOpaque];
  TabStop := True; // so that it can get keyboard focus

  FEncoding   := veAutoDetect;
  FViewerControlMode := vcmText;
  FCustom     := nil;
  FFileName   := '';
  FMappedFile := nil;
  FFileHandle := 0;
  FMappingHandle := 0;
  FPosition  := 0;
  FHPosition := 0;
  FHLowEnd   := 0;
  FLowLimit  := 0;
  FHighLimit := 0;
  FBOMLength := 0;
  FTextHeight:= 14; // dummy value
  FColCount  := 1;
  FTabSpaces := 8;
  FLeftMargin := 4;
  FMaxTextWidth := 1024;
  FAutoCopy := True;

  FLineList := TPtrIntList.Create;

  FUpdateScrollBarPos := True;
  FScrollBarPosition  := 0;
  FHScrollBarPosition := 0;

  FOnPositionChanged := nil;
  FOnGuessEncoding   := nil;

  FHex:=TCustomCharsPresentation.Create(16,2,8,8,@HexToValueProc);
  FDec:=TCustomCharsPresentation.Create(15,3,8,5,@DecToValueProc);  // for set bigger ValuePerLine need to improve method GetEndOfLine
end;

destructor TViewerControl.Destroy;
begin
  FHex.Free;
  FDec.Free;

  FHex:=nil;
  FDec:=nil;
  FCustom:=nil;

  UnMapFile;

  if Assigned(FLineList) then
    FreeAndNil(FLineList);

  inherited Destroy;
end;

procedure TViewerControl.DrawLastError;
var
  AStyle: TTextStyle;
begin
  AStyle:= Canvas.TextStyle;
  AStyle.Alignment:= taCenter;
  AStyle.Layout:= tlCenter;
  Canvas.Pen.Color := Canvas.Font.Color;
  Canvas.Line(0, 0, ClientWidth - 1, ClientHeight - 1);
  Canvas.Line(0, ClientHeight - 1, ClientWidth - 1, 0);
  Canvas.TextRect(GetClientRect, 0, 0, FLastError, AStyle);
end;

procedure TViewerControl.Paint;
var
  AText: String;
  oldTextHeight: Integer;
begin
  if not IsFileOpen then
  begin
    DrawLastError;
    Exit;
  end;

  if FShowCaret and FCaretVisible then
  begin
    FCaretPoint.X := -1;
    FCaretVisible := not LCLIntf.HideCaret(Handle);
  end;

  Canvas.Font := Self.Font;
  Canvas.Brush.Color := Self.Color;
  {$IF DEFINED(LCLQT) and (LCL_FULLVERSION < 093100)}
  Canvas.Brush.Style := bsSolid;
  Canvas.FillRect(ClientRect);
  {$ENDIF}
  Canvas.Brush.Style := bsClear;

  oldTextHeight:= FTextHeight;
  FTextHeight := Canvas.TextHeight('Wg') + FExtraLineSpacing;
  if oldTextHeight <> FTextHeight then
    DoShowCaret;

  if FViewerControlMode = vcmBook then
    FTextWidth := ((ClientWidth - (Canvas.TextWidth('W') * FColCount)) div FColCount)
  else begin
    AText := StringOfChar('W', FMaxTextWidth);
    FTextWidth := Canvas.TextFitInfo(AText, GetClientRect.Width - FLeftMargin);
  end;

  FLineList.Clear;

  case FViewerControlMode of
    vcmBin : WriteBin;
    vcmText: WriteText;
    vcmWrap: WriteText;
    vcmBook: WriteText;
    vcmDec,vcmHex : WriteCustom;
  end;

  if FShowCaret and (FCaretPoint.X > -1) then
  begin
    LCLIntf.SetCaretPos(FCaretPoint.X, FCaretPoint.Y);
    if not FCaretVisible then FCaretVisible:= LCLIntf.ShowCaret(Handle);
  end;
end;

procedure TViewerControl.SetViewerMode(Value: TViewerControlMode);

  procedure adjustPosByBOM;
  var
    offset: Integer;
  begin
    offset:= GetOffsetOfMode(FViewerControlMode) - GetOffsetOfMode(Value);
    Inc( FBlockBeg, offset );
    Inc( FBlockEnd, offset );
    Inc( FCaretPos, offset );
  end;

begin
  if not (csDesigning in ComponentState) then
  begin
    FLineList.Clear; // do not use cache from previous mode

    adjustPosByBOM;

    FViewerControlMode := Value;
    case FViewerControlMode of
      vcmHex: FCustom := FHex;
      vcmDec: FCustom := FDec;
    else
      FCustom := nil;
    end;

    if not IsFileOpen then
      Exit;

    // Take limits into account for selection.
    FBlockBeg := FBlockBeg + (GetDataAdr - FMappedFile);
    FBlockEnd := FBlockEnd + (GetDataAdr - FMappedFile);

    FHPosition := 0;
    FBOMLength := GetBomLength;
    UpdateLimits;

    // Take limits into account for selection.
    FBlockBeg := FBlockBeg - (GetDataAdr - FMappedFile);
    FBlockEnd := FBlockEnd - (GetDataAdr - FMappedFile);

    UpdateSelectionAndCaret;

    // Force recalculating position.
    SetPosition(FPosition, True);
    SetHPosition(FHPosition, True);

    UpdateScrollbars;
    Invalidate;
  end
  else
    FViewerControlMode := Value;
end;

procedure TViewerControl.SetColCount(const AValue: Integer);
begin
  if AValue > 0 then FColCount := AValue
    else FColCount := 1;
end;

procedure TViewerControl.SetMaxTextWidth(const AValue: Integer);
begin
  if AValue < 80 then
    FMaxTextWidth := 80
  else if AValue > 16384 then
    FMaxTextWidth := 16384
  else
    FMaxTextWidth:= AValue;
end;

procedure TViewerControl.SetTabSpaces(const AValue: Integer);
begin
  if AValue < 1 then
    FTabSpaces := 1
  else if AValue > 32 then
    FTabSpaces := 32
  else
    FTabSpaces := AValue;
end;

function TViewerControl.ScrollPosition(var aPosition: PtrInt; iLines: Integer): Boolean;
var
  i:      Integer;
  NewPos: PtrInt;
begin
  Result := False;
  NewPos := aPosition;
  if iLines < 0 then
    for i := 1 to -iLines do
      NewPos := GetStartOfPrevLine(NewPos)
  else
    for i := 1 to iLines do
      NewPos := GetStartOfNextLine(NewPos);

  Result    := aPosition <> NewPos;
  aPosition := NewPos;
end;

function TViewerControl.Scroll(iLines: Integer): Boolean;
var
  aPosition: PtrInt;
begin
  if not IsFileOpen then
    Exit(False);
  aPosition := FPosition;
  Result := ScrollPosition(aPosition, iLines);
  if aPosition <> FPosition then
    SetPosition(aPosition);
end;

function TViewerControl.HScroll(iSymbols: Integer): Boolean;
var
  newPos: Integer;
begin
  if not IsFileOpen then
    Exit(False);
  newPos := FHPosition + iSymbols;
  if newPos < 0 then
    newPos := 0
  else if (newPos > FHLowEnd - FTextWidth) and (FHLowEnd - FTextWidth > 0) then
    newPos := FHLowEnd - FTextWidth;
  if newPos <> FHPosition then
    SetHPosition(newPos);
  Result:= True;
end;

function TViewerControl.GetText(const StartPos, Len: PtrInt; const Xoffset: Integer): string;
begin
  SetString(Result, GetDataAdr + StartPos, Len);
  Result := TransformText(ConvertToUTF8(Result), Xoffset);
end;

procedure TViewerControl.SetText(const AValue: String);
begin
  UnMapFile;
  FText:= AValue;
  FileName:= EmptyStr;
  FFileSize:= Length(FText);
  FMappedFile:= Pointer(FText);
end;

procedure TViewerControl.CreateWnd;
begin
  inherited CreateWnd;
  UpdateScrollbars;
end;

procedure TViewerControl.ScrollBarSetPosition(Which, Value: Integer);
var
  ScrollVisible: Boolean;
  ScrollInfo: TScrollInfo;
begin
  if HandleAllocated then
  begin
    ScrollVisible:= (Which = SB_VERT) or (FViewerControlMode = vcmText);
    ShowScrollBar(Handle, Which, ScrollVisible);

    if ScrollVisible then
    begin
      ScrollInfo:= Default(TScrollInfo);
      ScrollInfo.cbSize:= SizeOf(ScrollInfo);
      ScrollInfo.fMask:= SIF_POS or SIF_RANGE or SIF_PAGE;
      ScrollInfo.nPage:= 1;
      ScrollInfo.nMax:= 100;
      ScrollInfo.nPos:= Value;
      SetScrollInfo(Handle, Which, ScrollInfo, ScrollVisible);
    end;
  end;
end;

function TViewerControl.ScrollBarGetPosition(Which: Integer): Integer;
var
  ScrollInfo: TScrollInfo;
begin
  if HandleAllocated then
  begin
    ScrollInfo:= Default(TScrollInfo);
    ScrollInfo.cbSize:= SizeOf(ScrollInfo);
    ScrollInfo.fMask:= SIF_POS;
    if GetScrollInfo(Handle, Which, ScrollInfo) then
      Exit(ScrollInfo.nPos);
  end;
  case Which of
    SB_VERT: Result:= FScrollBarPosition;
    SB_HORZ: Result:= FHScrollBarPosition;
  end;
end;

procedure TViewerControl.WMVScroll(var Message: TLMVScroll);
begin
  FUpdateScrollBarPos := False;
  case Message.ScrollCode of
    SB_LINEUP:     Scroll(-1);
    SB_LINEDOWN:   Scroll(1);
    SB_PAGEUP:     PageUp;
    SB_PAGEDOWN:   PageDown;
    SB_TOP:        GoHome;
    SB_BOTTOM:     GoEnd;
    SB_THUMBTRACK,
    SB_THUMBPOSITION:
      begin
        // This check helps avoiding loops if changing
        // ScrollBarPosition below triggers another SB_THUMBPOSITION message.
        if (Message.ScrollCode = SB_THUMBTRACK) or (Message.Pos <> FScrollBarPosition) then
        begin
          if Message.Pos = 0 then
            GoHome
          else if Message.Pos = 100 then
            GoEnd
          else begin
            Percent := Message.Pos;
          end;
        end;
        if (Message.ScrollCode = SB_THUMBPOSITION) then
        begin
          ScrollBarSetPosition(SB_VERT, FScrollBarPosition);
        end;
      end;
    SB_ENDSCROLL:
      begin
      end;
  end;
  FUpdateScrollBarPos := True;
end;

procedure TViewerControl.WMHScroll(var Message: TLMHScroll);
begin
  FUpdateScrollBarPos := False;
  case Message.ScrollCode of
    SB_LINELEFT:   HScroll(-1);
    SB_LINERIGHT:  HScroll(1);
    SB_PAGELEFT:   HPageUp;
    SB_PAGERIGHT:  HPageDown;
    SB_LEFT:       HGoHome;
    SB_RIGHT:      HGoEnd;
    SB_THUMBTRACK,
    SB_THUMBPOSITION:
      begin
        // This check helps avoiding loops if changing
        // ScrollBarPosition below triggers another SB_THUMBPOSITION message.
        if (Message.ScrollCode = SB_THUMBTRACK) or (Message.Pos <> FHScrollBarPosition) then
        begin
          if Message.Pos = 0 then
            HGoHome
          else if Message.Pos = 100 then
            HGoEnd
          else begin
            HScroll((FHLowEnd - FTextWidth) * Message.Pos div 100 - FHPosition);
          end;
        end;
        if (Message.ScrollCode = SB_THUMBPOSITION) then
        begin
          ScrollBarSetPosition(SB_HORZ, FHScrollBarPosition);
        end;
      end;
    SB_ENDSCROLL:
      begin
      end;
  end;
  FUpdateScrollBarPos := True;
end;

procedure TViewerControl.WMSetFocus(var Message: TLMSetFocus);
begin
  if FShowCaret then
    DoShowCaret;
end;

procedure TViewerControl.WMKillFocus(var Message: TLMKillFocus);
begin
  if FShowCaret then
  begin
    FCaretVisible:= False;
    LCLIntf.DestroyCaret(Handle);
  end;
end;

procedure TViewerControl.FontChanged(Sender: TObject);
begin
  inherited FontChanged(Sender);

  if HandleAllocated then
  begin
    FTextHeight := Canvas.TextHeight('Wg') + FExtraLineSpacing;
    if FShowCaret then
      DoShowCaret;
  end;
end;

function TViewerControl.CalcTextLineLength(var iStartPos: PtrInt; const aLimit: Int64; out DataLength: PtrInt): Integer;
var
  MaxLineLength: Boolean;
  CharLenInBytes: Integer;
  OldPos, LastSpacePos: PtrInt;
  LastSpaceResult: Integer;
begin
  Result := 0;
  DataLength := 0;
  LastSpacePos := -1;
  MaxLineLength := True;
  OldPos := iStartPos;

  while MaxLineLength and (iStartPos < aLimit) do
  begin
    case GetNextCharAsAscii(iStartPos, CharLenInBytes) of
      9:             // tab
        Inc(Result, FTabSpaces - Result mod FTabSpaces);
      10:            // stroka
        begin
          DataLength := iStartPos - OldPos;
          iStartPos := iStartPos + CharLenInBytes;
          Exit;
        end;
      13:             // karetka
        begin
          DataLength := iStartPos - OldPos;
          iStartPos := iStartPos + CharLenInBytes;
          // Move after possible #10.
          if (iStartPos < aLimit) and (GetNextCharAsAscii(iStartPos, CharLenInBytes) = 10) then
            Inc(iStartPos, CharLenInBytes);
          Exit;
        end;
      32, 33, 40, 41, 44, 45, 46, 47, 92, 58, 59, 63, 91, 93:              //probel
        begin
          Inc(Result, 1);
          LastSpacePos := iStartPos + CharLenInBytes;
          LastSpaceResult := Result;
        end;
      else
        Inc(Result, 1);
    end;

    if CharLenInBytes = 0 then  // End of data or invalid character.
      break;

    iStartPos := iStartPos + CharLenInBytes;
    DataLength := iStartPos - OldPos;
    case FViewerControlMode of
    vcmText:   MaxLineLength := Result < FMaxTextWidth;
    vcmWrap:   MaxLineLength := Result < FTextWidth;
    vcmBook:   MaxLineLength := Canvas.TextWidth(GetText(OldPos, DataLength, 0)) < FTextWidth;
    else
      Exit;
    end;
  end;

  if (not MaxLineLength) and (LastSpacePos <> -1) then
  begin
    iStartPos := LastSpacePos;
    Result := LastSpaceResult;
    DataLength := iStartPos - OldPos;
  end;
end;

function TViewerControl.TransformText(const sText: String; const Xoffset: Integer): String;
var
  c: AnsiChar;
  i: Integer;
  Dos: Boolean;
begin
  Result := '';
  Dos:= FEncoding in ViewerEncodingOem;
  for i := 1 to Length(sText) do
  begin
    c := sText[i];
    // Parse only ASCII chars.
    case c of
      #9:
        Result := Result + StringOfChar(' ',
                    FTabSpaces - (UTF8Length(Result) + Xoffset) mod FTabSpaces);
      else
        begin
          if c < ' ' then
          begin
            if Dos then
              Result := Result + ASCII_TABLE[Ord(c)]
            else
              Result := Result + ' ';
          end
          else
            Result := Result + c;
        end;
    end;
  end;
end;

function TViewerControl.TransformBin(var aPosition: PtrInt; aLimit: PtrInt): String;
var
  S: String;
  C: AnsiChar;
  P: PAnsiChar;
  Len: Integer;
  I, L: Integer;
  SingleByte: Boolean;
begin
  Result := EmptyStr;

  if (APosition + cBinWidth) > aLimit then
    Len:= aLimit - APosition
  else begin
    Len:= cBinWidth;
  end;

  SetString(S, PAnsiChar(GetDataAdr) + aPosition, Len);
  SingleByte:= not (FEncoding in ViewerEncodingMultiByte);

  if SingleByte then
  begin
    S:= ConvertToUTF8(S);
  end;
  L:= Length(S);
  P:= PAnsiChar(S);

  for I := 1 to L do
  begin
    C := P^;
    if C < ' ' then
      Result := Result + '.'
    else if SingleByte then
      Result := Result + C
    else if C > #127 then
      Result := Result + '.'
    else begin
      Result := Result + C;
    end;
    Inc(P);
  end;
  Inc(aPosition, Len);
end;

function TViewerControl.TransformHex(var aPosition: PtrInt; aLimit: PtrInt): AnsiString;
begin
  Result:=TransformCustom(aPosition,aLimit);
end;

function TViewerControl.TransformCustom(var APosition: PtrInt; ALimit: PtrInt;
  AWithAdditionalData: Boolean): String;
var
  sAscii: string = '';
  sRez  : string = '';
  tPos  : integer;
begin

  tPos:=APosition;
  sRez:=TransformCustomBlock(APosition,FCustom.ValuesPerLine,True,True,sAscii);

//  Result := LineFormat(sRez, sStr, aStartOffset) else

  if AWithAdditionalData then
  begin
    sRez := Format('%s: %s', [IntToHex(tPos, FCustom.MaxAddrDigits), sRez]);

    if Length(sRez) < FCustom.ValuesPerLine * (FCustom.SpaceCount+FCustom.MaxValueDigits) then
      sRez := sRez + StringOfChar(' ', FCustom.ValuesPerLine * (FCustom.SpaceCount+FCustom.MaxValueDigits) - Length(sRez));
    sRez := sRez + '  ';
    sRez := sRez + sAscii;
  end;

  Result:=sRez;
end;

function TViewerControl.TransformCustomBlock(var APosition: PtrInt;
  DataLength: Integer; ASeparatorsOn, AAlignData: Boolean; out AChars: String): String;
var
  S: String;
  C: AnsiChar;
  P: PAnsiChar;
  Len: Integer;
  I, L: Integer;
  sEmpty: String;
  iSep: Integer = 1;
  SingleByte: Boolean;
begin
  Result:= EmptyStr;

  if (APosition + DataLength) > FHighLimit then
    Len:= FHighLimit - APosition
  else begin
    Len:= DataLength;
  end;

  SetString(S, PAnsiChar(GetDataAdr) + aPosition, Len);
  SingleByte:= not (FEncoding in ViewerEncodingMultiByte);

  if SingleByte then
  begin
    S:= ConvertToUTF8(S);
  end;
  L:= Length(S);
  P:= PAnsiChar(S);
  AChars:= EmptyStr;

  for I := 1 to L do
  begin
    C := P^;
    if C < ' ' then
      AChars := AChars + '.'
    else if SingleByte then
      AChars := AChars + C
    else if C > #127 then
      AChars := AChars + '.'
    else begin
      AChars := AChars + C;
    end;
    Inc(P);
  end;
  P:= PAnsiChar(GetDataAdr);

  for I := 0 to Len - 1 do
  begin
    C := P[aPosition];
    Result += FCustom.ChrToValueProc(C, FCustom.MaxValueDigits);

    if (iSep  =  FCustom.CountSeperate) and ASeparatorsOn and
       (I     < (FCustom.ValuesPerLine - 1))then
    begin
      iSep := 0;
      Result += FCustom.SeparatorChar;
    end else
    begin
      Result += FCustom.SeparatorSpace;
    end;

    Inc(aPosition);
    Inc(iSep);
  end;

  if AAlignData then
  begin
    sEmpty := StringOfChar(#32, FCustom.MaxValueDigits);

    while (I < FCustom.ValuesPerLine - 1) do
    begin
      Result += sEmpty + FCustom.SeparatorSpace;
      Inc(I);
    end;
  end;
end;


function TViewerControl.DecToValueProc(AChar:AnsiChar;AMaxDigitsCount:integer):AnsiString;
begin
  Result:= IntToStr(Ord(AChar));
  while Length(Result) < AMaxDigitsCount do
    Result:= '0' + Result;
end;

function TViewerControl.HexToValueProc(AChar:AnsiChar;AMaxDigitsCount:integer):AnsiString;
begin
  Result:=IntToHex(Ord(AChar), AMaxDigitsCount);
  while length(Result)<AMaxDigitsCount do
        Result:=' '+Result;
end;

function TViewerControl.GetStartOfLine(aPosition: PtrInt): PtrInt;

  function GetStartOfLineText: PtrInt;
  var
    tmpPos, LineStartPos: PtrInt;
    DataLength: PtrInt;
    prevChar: Cardinal;
    MaxLineLength: Boolean;
    CharLenInBytes: Integer;
  begin
    prevChar := GetPrevCharAsAscii(aPosition, CharLenInBytes);
    if CharLenInBytes = 0 then
      Exit(aPosition);

    // Check if this already is not a start of line (if previous char is #10).
    if prevChar = 10 then
      Exit(aPosition);

    tmpPos := aPosition - CharLenInBytes;

    if tmpPos <= FLowLimit then
      Exit(FLowLimit);

    // Check if we're not in the middle of line ending
    // (previous char is #13, current char is #10).
    if (prevChar = 13) and
       (GetNextCharAsAscii(aPosition, CharLenInBytes) = 10) then
    begin
      prevChar := GetPrevCharAsAscii(tmpPos, CharLenInBytes);
      if CharLenInBytes = 0 then
        Exit(aPosition);
      Dec(tmpPos, CharLenInBytes);
    end;

    if tmpPos <= FLowLimit then
      Exit(FLowLimit);

    DataLength:= 0;
    // Search for real start of line.
    while (not (prevChar in [10, 13])) and (tmpPos > FLowLimit) do
    begin
      prevChar := GetPrevCharAsAscii(tmpPos, CharLenInBytes);
      if CharLenInBytes = 0 then
        Break;
      Dec(tmpPos, CharLenInBytes);

      case prevChar of
      9:
        Inc(DataLength, FTabSpaces - DataLength mod FTabSpaces);
      else
        Inc(DataLength, 1);
      end;

      case FViewerControlMode of
      vcmText:   MaxLineLength := DataLength < FMaxTextWidth;
      vcmWrap:   MaxLineLength := DataLength < FTextWidth;
      end;
      if not MaxLineLength then Exit(tmpPos);
    end;

    // Previous end of line not found and there are no more data to check.
    if (not (prevChar in [10, 13])) and (tmpPos <= FLowLimit) then
      Exit(FLowLimit);

    // Move forward to first non-line ending character.
    Inc(tmpPos, CharLenInBytes);

    // Search for start of real line or wrapped line.
    while True do
    begin
      LineStartPos := tmpPos;
      CalcTextLineLength(tmpPos, FHighLimit, DataLength);

      if tmpPos = aPosition then
      begin
        if aPosition < FHighLimit then
          Exit(aPosition)       // aPosition is already at start of a line
        else
          Exit(LineStartPos);   // aPosition points to end of file so return start of this line
      end
      else if tmpPos > aPosition then
        Exit(LineStartPos);     // Found start of line
    end;
  end;

  function GetStartOfLineFixed(aFixedWidth: Integer): PtrInt;
  begin
    Result := aPosition - (aPosition mod aFixedWidth);
  end;

var
  i: Integer;
begin
  if aPosition <= FLowLimit then
    Exit(FLowLimit)
  else if aPosition >= FHighLimit then
    aPosition := FHighLimit;   // search from the end of the file

  // Speedup for currently displayed positions.
  if (FLineList.Count > 0) and
     (aPosition >= FLineList.Items[0]) and
     (aPosition <= FLineList.Items[FLineList.Count - 1]) then
  begin
    for i := FLineList.Count - 1 downto 0 do
      if FLineList.Items[i] <= aPosition then
        Exit(FLineList.Items[i]);
  end;

  case FViewerControlMode of
    vcmBin:
      Result := GetStartOfLineFixed(cBinWidth);
    vcmHex, vcmDec:
      Result := GetStartOfLineFixed(FCustom.ValuesPerLine);

    vcmText, vcmWrap, vcmBook:
      Result := GetStartOfLineText;
    else
      Result := aPosition;
  end;
end;

function TViewerControl.GetEndOfLine(aPosition: PtrInt): PtrInt;

  function GetEndOfLineText: PtrInt;
  var
    tmpPos: PtrInt;
    DataLength: PtrInt;
  begin
    Result := GetStartOfLine(aPosition);
    tmpPos := Result;
    CalcTextLineLength(tmpPos, FHighLimit, DataLength);
    Result := Result + DataLength;
    if Result < aPosition then
      Result := aPosition;
  end;

  function GetEndOfLineFixed(aFixedWidth: Integer): PtrInt;
  begin
    Result := aPosition - (aPosition mod aFixedWidth) + aFixedWidth;
  end;

begin
  case FViewerControlMode of
    vcmBin:
      Result := GetEndOfLineFixed(cBinWidth);
    vcmHex,vcmDec:
      Result := GetEndOfLineFixed(FCustom.ValuesPerLine);
    vcmText, vcmWrap, vcmBook:
      Result := GetEndOfLineText;
    else
      Result := aPosition;
  end;
end;

function TViewerControl.GetStartOfPrevLine(aPosition: PtrInt): PtrInt;

  function GetPrevLineText: PtrInt;
  var
    tmpPos, LineStartPos: PtrInt;
    DataLength: PtrInt;
    prevChar: Cardinal;
    MaxLineLength: Boolean;
    CharLenInBytes: Integer;
  begin
    prevChar := GetPrevCharAsAscii(aPosition, CharLenInBytes);
    if CharLenInBytes = 0 then
      Exit(aPosition);

    tmpPos := aPosition - CharLenInBytes; // start search from previous character

    if tmpPos <= FLowLimit then
      Exit(FLowLimit);

    // Check if we're not in the middle of line ending
    // (previous char is #13, current char is #10).
    if (prevChar = 13) and
       (GetNextCharAsAscii(aPosition, CharLenInBytes) = 10) then
    begin
      prevChar := GetPrevCharAsAscii(tmpPos, CharLenInBytes);
      if CharLenInBytes = 0 then
        Exit(aPosition);
      Dec(tmpPos, CharLenInBytes);
    end
    else
    begin
      // Bypass possible end of previous line.
      if prevChar = 10 then
      begin
        prevChar := GetPrevCharAsAscii(tmpPos, CharLenInBytes);
        if CharLenInBytes = 0 then
          Exit(aPosition);
        Dec(tmpPos, CharLenInBytes);
      end;
      if prevChar = 13 then
      begin
        prevChar := GetPrevCharAsAscii(tmpPos, CharLenInBytes);
        if CharLenInBytes = 0 then
          Exit(aPosition);
        Dec(tmpPos, CharLenInBytes);
      end;
    end;

    if tmpPos <= FLowLimit then
      Exit(FLowLimit);

    DataLength:= 0;
    // Search for real start of line.
    while (not (prevChar in [10, 13])) and (tmpPos > FLowLimit) do
    begin
      prevChar := GetPrevCharAsAscii(tmpPos, CharLenInBytes);
      if CharLenInBytes = 0 then
        Break;
      Dec(tmpPos, CharLenInBytes);

      case prevChar of
      9:
        Inc(DataLength, FTabSpaces - DataLength mod FTabSpaces);
      else
        Inc(DataLength, 1);
      end;

      case FViewerControlMode of
      vcmText:   MaxLineLength := DataLength < FMaxTextWidth;
      vcmWrap:   MaxLineLength := DataLength < FTextWidth;
      end;
      if not MaxLineLength then Exit(tmpPos);
    end;

    // Move forward to first non-line ending character.
    Inc(tmpPos, CharLenInBytes);

    // Search for start of real line or wrapped line.
    while True do
    begin
      LineStartPos := tmpPos;
      CalcTextLineLength(tmpPos, aPosition, DataLength);

      if tmpPos >= aPosition then
        Exit(LineStartPos);  // Found start of line
    end;
  end;

  function GetPrevLineFixed(aFixedWidth: Integer): PtrInt;
  begin
    Result := aPosition - (aPosition mod aFixedWidth);
    if Result >= aFixedWidth then
      Result := Result - aFixedWidth;
  end;

var
  i: Integer;
begin
  if aPosition <= FLowLimit then
    Exit(FLowLimit)
  else if aPosition >= FHighLimit then
    aPosition := FHighLimit;   // search from the end of the file

  // Speedup for currently displayed positions.
  if (FLineList.Count > 0) and
     (aPosition >= FLineList.Items[0]) and
     (aPosition <= FLineList.Items[FLineList.Count - 1]) then
  begin
    for i := FLineList.Count - 1 downto 0 do
      if FLineList.Items[i] < aPosition then
        Exit(FLineList.Items[i]);
  end;

  case FViewerControlMode of
    vcmBin:
      Result := GetPrevLineFixed(cBinWidth);
    vcmHex,vcmDec:
      Result := GetPrevLineFixed(FCustom.ValuesPerLine);
    vcmText, vcmWrap, vcmBook:
      Result := GetPrevLineText;
    else
      Result := aPosition;
  end;
end;

function TViewerControl.GetStartOfNextLine(aPosition: PtrInt): PtrInt;

  function GetNextLineText: PtrInt;
  var
    tmpPos: PtrInt;
    DataLength: PtrInt;
    prevChar: Cardinal;
    CharLenInBytes: Integer;
  begin
    tmpPos := aPosition;

    // This might not be a real start of line (it may be start of wrapped line).
    // Search for start of line.
    while (tmpPos > FLowLimit) do
    begin
      prevChar := GetPrevCharAsAscii(tmpPos, CharLenInBytes);
      if CharLenInBytes = 0 then
        Break;
      if (prevChar in [10, 13]) then
        Break
      else
        Dec(tmpPos, CharLenInBytes);
    end;

    // Now we know we are at the start of a line, search the start of next line.
    while True do
    begin
      CalcTextLineLength(tmpPos, FHighLimit, DataLength);

      if tmpPos >= aPosition then
        Exit(tmpPos);  // Found start of line
    end;
  end;

  function GetNextLineFixed(aFixedWidth: Integer): PtrInt;
  begin
    Result := aPosition - (aPosition mod aFixedWidth);
    if Result + aFixedWidth < FHighLimit then
      Result := Result + aFixedWidth;
  end;

var
  i: Integer;
begin
  if aPosition < FLowLimit then
    aPosition := FLowLimit         // search from the start of the file
  else if aPosition >= FHighLimit then
    aPosition := FHighLimit;   // search from the end of the file

  // Speedup for currently displayed positions.
  if (FLineList.Count > 0) and
     (aPosition >= FLineList.Items[0]) and
     (aPosition <= FLineList.Items[FLineList.Count - 1]) then
  begin
    for i := 0 to FLineList.Count - 1 do
      if FLineList.Items[i] > aPosition then
        Exit(FLineList.Items[i]);
  end;

  case FViewerControlMode of
    vcmBin:
      Result := GetNextLineFixed(cBinWidth);
    vcmHex,vcmDec:
      Result := GetNextLineFixed(FCustom.ValuesPerLine);
    vcmText, vcmWrap, vcmBook:
      Result := GetNextLineText;
    else
      Result := aPosition;
  end;
end;

procedure TViewerControl.PageUp;
var
  H: Integer;
begin
  H := GetClientHeightInLines * FColCount - 1;
  if H <= 0 then
    H := 1;
  Scroll(-H);
end;

procedure TViewerControl.HPageUp;
var
  H: Integer;
begin
  H := FHPosition - FTextWidth;
  if H <= 0 then
    H := FHPosition else H:= FTextWidth;
  HScroll(-H);
end;

procedure TViewerControl.PageDown;
var
  H: Integer;
begin
  H := GetClientHeightInLines * FColCount - 1;
  if H <= 0 then
    H := 1;
  Scroll(H);
end;

procedure TViewerControl.HPageDown;
var
  H: Integer;
begin
  H := FHLowEnd - FHPosition;
  if H > FTextWidth then H := FTextWidth ;
  HScroll(H);
end;

procedure TViewerControl.GoHome;
begin
  Position := FLowLimit;
end;

procedure TViewerControl.GoEnd;
begin
  Position := FHighLimit;
end;

procedure TViewerControl.HGoHome;
begin
  HScroll (-FHPosition);
end;

procedure TViewerControl.HGoEnd;
begin
  HScroll (FHLowEnd-FHPosition);
end;

procedure TViewerControl.CaretGoHome;
begin
  HScroll (-FHPosition);
  CaretPos := GetStartOfLine(CaretPos);
end;

procedure TViewerControl.CaretGoEnd;
begin
  if FViewerControlMode in [vcmBin, vcmHex, vcmDec] then
    CaretPos := GetEndOfLine(CaretPos) - 1
  else begin
    CaretPos := GetEndOfLine(CaretPos);
  end;

  if FViewerControlMode = vcmText then
  begin
    if not IsVisible(CaretPos) then
    begin
      if (FVisibleOffset < FHPosition) or
         (FVisibleOffset > FHPosition + FTextWidth) then
      begin
        SetHPosition(FVisibleOffset);
        HScroll(-1);
      end;
    end;
  end;
end;

procedure TViewerControl.SetFileName(const sFileName: String);
begin
  if not (csDesigning in ComponentState) then
  begin
    UnMapFile;

    if sFileName <> '' then
    begin
      if MapFile(sFileName) then
      begin
        FFileName  := sFileName;

        // Detect encoding if needed.
        if FEncoding = veAutoDetect then
          FEncoding := DetectEncoding;

        ReReadFile;

        CaretPos := FLowLimit;
      end;
    end;
  end
  else
    FFileName  := sFileName;
end;

function TViewerControl.MapFile(const sFileName: String): Boolean;

  function ReadFile: Boolean; inline;
  begin
    FMappedFile := GetMem(FFileSize);
    Result := (FileRead(FFileHandle, FMappedFile^, FFileSize) = FFileSize);
    if not Result then
    begin
      FLastError := mbSysErrorMessage;
      FreeMemAndNil(FMappedFile);
    end;
    FileClose(FFileHandle);
    FFileHandle := 0;
  end;

{$IFDEF LINUX}
var
  Sbfs: TStatFS;
{$ENDIF}
begin
  Result := False;
  FLastError := EmptyStr;

  if Assigned(FMappedFile) then
    UnMapFile; // if needed

  if Assigned(FOnFileOpen) then
    FFileHandle := FOnFileOpen(sFileName, fmOpenRead or fmShareDenyNone)
  else begin
    FFileHandle := mbFileOpen(sFileName, fmOpenRead or fmShareDenyNone);
  end;

  if FFileHandle = feInvalidHandle then
  begin
    FLastError := mbSysErrorMessage;
    FFileHandle := 0;
    Exit;
  end;

  FFileSize := FileGetSize(FFileHandle);

  if (FFileSize < 0) then
  begin
    FLastError := mbSysErrorMessage;
    FileClose(FFileHandle);
    FFileHandle := 0;
    Exit;
  end;

{$IFDEF LINUX}
  if (fpFStatFS(FFileHandle, @Sbfs) = 0) then
  begin
    // Special case for PROC_FS and SYS_FS
    if (sbfs.fstype = PROC_SUPER_MAGIC) or (sbfs.fstype = SYSFS_MAGIC) then
    begin
      FMappedFile := GetMem(MaxMemSize - 1);
      FFileSize := FileRead(FFileHandle, FMappedFile^, MaxMemSize - 1);
      Result := (FFileSize >= 0);
      if not Result then
      begin
        FLastError := mbSysErrorMessage;
        FreeMemAndNil(FMappedFile);
      end;
      FileClose(FFileHandle);
      FFileHandle := 0;
      Exit;
    end;
  end;
{$ENDIF}

  if (FFileSize < MaxMemSize) then
  begin
    Result := ReadFile;
    Exit;
  end;

{$IFDEF MSWINDOWS}
  FMappingHandle := CreateFileMapping(FFileHandle, nil, PAGE_READONLY, 0, 0, nil);
  if FMappingHandle = 0 then
  begin
    FLastError := mbSysErrorMessage;
    FMappedFile := nil;
    UnMapFile;
  end
  else begin
    FMappedFile := MapViewOfFile(FMappingHandle, FILE_MAP_READ, 0, 0, 0);
    if (FMappedFile = nil) then
    begin
      FLastError := mbSysErrorMessage;
      UnMapFile;
    end;
  end;
{$ELSE}
  FMappedFile := fpmmap(nil, FFileSize, PROT_READ, MAP_PRIVATE{SHARED}, FFileHandle, 0);
  if FMappedFile = MAP_FAILED then
  begin
    FLastError := mbSysErrorMessage;
    FMappedFile:= nil;
    FileClose(FFileHandle);
    FFileHandle := 0;
    Exit;
  end;
{$ENDIF}
  Result := Assigned(FMappedFile);
end;

procedure TViewerControl.UnMapFile;
begin
  if FMappedFile = Pointer(FText) then
  begin
    FMappedFile:= nil;
    FText:= EmptyStr;
  end;

  if (FFileSize < MaxMemSize) then
  begin
    if Assigned(FMappedFile) then
    begin
      FreeMem(FMappedFile);
      FMappedFile := nil;
    end;
  end;

{$IFDEF MSWINDOWS}

  if Assigned(FMappedFile) then
  begin
    UnmapViewOfFile(FMappedFile);
    FMappedFile := nil;
  end;

  if FMappingHandle <> 0 then
  begin
    CloseHandle(FMappingHandle);
    FMappingHandle := 0;
  end;

{$ELSE}

  if Assigned(FMappedFile) then
  begin
    if fpmunmap(FMappedFile, FFileSize) = -1 then
      DebugLn('Error unmapping file: ', SysErrorMessage(fpgeterrno));
    FMappedFile := nil;
  end;

{$ENDIF}

  if FFileHandle <> 0 then
  begin
    FileClose(FFileHandle);
    FFileHandle := 0;
  end;

  FFileName  := '';
  FFileSize  := 0;
  Position   := 0;
  FLowLimit  := 0;
  FHighLimit := 0;
  FBOMLength := 0;
  FBlockBeg  := 0;
  FBlockEnd  := 0;
end;

procedure TViewerControl.WriteText;
var
  yIndex, xIndex, w, i: Integer;
  LineStart, iPos: PtrInt;
  CharLenInBytes: Integer;
  DataLength: PtrInt;
  sText: String;

  procedure DrawCaret(X, Y: Integer; LinePos: PtrInt);
  begin
    if FShowCaret and (FCaretPos = LinePos) then
    begin
      FCaretPoint.X:= X;
      FCaretPoint.Y:= Y;
    end;
  end;

begin
  iPos := FPosition;
  if Mode = vcmBook then
     w := Width div FColCount
  else begin
     w := 0;
  end;

  for xIndex := 0 to FColCount-1 do
  begin
    for yIndex := 0 to GetClientHeightInLines(False) - 1 do
    begin
      if iPos > FHighLimit then
        Break;

      if iPos = FHighLimit then
      begin
        if GetPrevCharAsAscii(iPos, CharLenInBytes) = 10 then
        begin
          DrawCaret(0, yIndex * FTextHeight, iPos);
        end;
        Break;
      end;

      AddLineOffset(iPos);
      LineStart := iPos;

      i := CalcTextLineLength(iPos, FHighLimit, DataLength);

      if i > FHLowEnd then FHLowEnd:= i;

      if DataLength = 0 then
        DrawCaret(0, yIndex * FTextHeight, LineStart)
      else begin
        if (Mode = vcmText) and (FHPosition > 0) then
        begin
          for i:= 1 to FHPosition do
          begin
            GetNextCharAsAscii(LineStart, CharLenInBytes);
            DataLength -= CharLenInBytes;
            LineStart += CharLenInBytes;
          end;
          if (DataLength <= 0) then Continue;
        end;
        sText := GetText(LineStart, DataLength, 0);

        OutText(FLeftMargin + xIndex * w, yIndex * FTextHeight, sText, LineStart, DataLength);
      end;
    end;
  end;
end;

procedure TViewerControl.WriteCustom;
// this method render visible page of text
var
  yIndex: Integer;
  iPos, LineStart: PtrInt;
  s: string;
begin
  iPos := FPosition;
  for yIndex := 0 to GetClientHeightInLines(False) - 1 do
  begin
    if iPos >= FHighLimit then
      Break;
    LineStart := iPos;
    AddLineOffset(iPos);
    s := TransformCustom(iPos, FHighLimit);  // get line text for render

    if s <> '' then
      OutCustom(FLeftMargin, yIndex * FTextHeight, s, LineStart, iPos - LineStart);  // render line to canvas
  end;
end;

procedure TViewerControl.WriteBin;
var
  yIndex: Integer;
  iPos, LineStart: PtrInt;
  s: string;
begin
  iPos := FPosition;
  for yIndex := 0 to GetClientHeightInLines(False) - 1 do
  begin
    if iPos >= FHighLimit then
      Break;
    LineStart := iPos;
    AddLineOffset(iPos);
    s := TransformBin(iPos, FHighLimit);
    if s <> '' then
      OutBin(FLeftMargin, yIndex * FTextHeight, s, LineStart, iPos - LineStart);
  end;
end;

function TViewerControl.GetDataAdr: Pointer;
begin
  Result := FMappedFile + GetOffsetOfMode(FViewerControlMode);
end;

procedure TViewerControl.SetPosition(Value: PtrInt);
begin
  SetPosition(Value, False);
end;

procedure TViewerControl.SetHPosition(Value: Integer);
begin
  SetHPosition(Value, False);
end;

procedure TViewerControl.SetHPosition(Value: Integer; Force: Boolean);
begin
  if not IsFileOpen then
    Exit;

  FHPosition := Value;
  // Set new scroll position.
  if (FHPosition > 0) and (FHLowEnd - FTextWidth > 0) then
    FHScrollBarPosition := FHPosition * 100 div (FHLowEnd - FTextWidth)
  else
    FHScrollBarPosition := 0;
  // Update scrollbar position.
  if FUpdateScrollBarPos then
  begin
    if ScrollBarGetPosition(SB_HORZ) <> FHScrollBarPosition then
    begin
      ScrollBarSetPosition(SB_HORZ, FHScrollBarPosition);
    end;
  end;
  // else the scrollbar position will be updated in ScrollBarVertScroll
  Invalidate;
end;

procedure TViewerControl.SetPosition(Value: PtrInt; Force: Boolean);
var
  LinesTooMany:    Integer;
  LastLineReached: Boolean;
begin
  if not IsFileOpen then
    Exit;

  // Double byte text can have only even position
  if (Encoding in ViewerEncodingDoubleByte) and Odd(Value) then
  begin
    Value := Value - 1;
  end;

  // Speedup if total nr of lines is less then nr of lines that can be displayed.
  if (FPosition = FLowLimit) and                // only if already at the top
     (FLineList.Count > 0) and (FLineList.Count < GetClientHeightInLines)
  then
    Value := FLowLimit
  else
    // Boundary checks are done in GetStartOfLine.
    Value := GetStartOfLine(Value);

  if (Value <> FPosition) or Force then
  begin
    // Don't allow empty lines at the bottom of the control.
    LinesTooMany := GetClientHeightInLines - GetLinesTillEnd(Value, LastLineReached);
    if LinesTooMany > 0 then
    begin
      // scroll back upwards
      ScrollPosition(Value, -LinesTooMany);
    end;

    FPosition := Value;
    if Assigned(FOnPositionChanged) then
      FOnPositionChanged(Self);
    Invalidate;

    // Set new scroll position.
    if LastLineReached and (Value > 0) then
      FScrollBarPosition := 100
    else
      FScrollBarPosition := Percent;
  end;

  // Update scrollbar position.
  if FUpdateScrollBarPos then
  begin
    if ScrollBarGetPosition(SB_VERT) <> FScrollBarPosition then
    begin
      ScrollBarSetPosition(SB_VERT, FScrollBarPosition);
    end;
  end;
  // else the scrollbar position will be updated in ScrollBarVertScroll
end;

procedure TViewerControl.SetEncoding(AEncoding: TViewerEncoding);
begin
  if not (csDesigning in ComponentState) then
  begin
    if AEncoding = veAutoDetect then
      FEncoding := DetectEncoding
    else
      FEncoding := AEncoding;

    ReReadFile;
  end
  else
    FEncoding := AEncoding;
end;

function TViewerControl.GetEncodingName: string;
begin
  Result := ViewerEncodingsNames[FEncoding];
end;

procedure TViewerControl.SetEncodingName(AEncodingName: string);
var
  i: TViewerEncoding;
begin
  for i := Low(TViewerEncoding) to High(TViewerEncoding) do
    if NormalizeEncoding(ViewerEncodingsNames[i]) = NormalizeEncoding(AEncodingName) then
    begin
      SetEncoding(i);
      break;
    end;
end;

function TViewerControl.GetClientHeightInLines(Whole: Boolean): Integer;
begin
  if FTextHeight > 0 then
  begin
    if Whole then
      Result := GetClientRect.Height div FTextHeight
    else
      Result := Ceil(GetClientRect.Height / FTextHeight);
  end
  else
    Result := 0;
end;

function TViewerControl.GetLinesTillEnd(FromPosition: PtrInt;
  out LastLineReached: Boolean): Integer;
var
  iPos: PtrInt;
  yIndex: Integer;
  DataLength: PtrInt;
  CharLenInBytes: Integer;
begin
  Result := 0;
  iPos   := FromPosition;
  for yIndex := 0 to GetClientHeightInLines - 1 do
  begin
    if iPos >= FHighLimit then
      Break;
    Inc(Result, 1);
    case Mode of
      vcmBin:
        iPos := iPos + cBinWidth;
      vcmHex,vcmDec:
        iPos := iPos + FCustom.ValuesPerLine;
      vcmText, vcmWrap, vcmBook:
        CalcTextLineLength(iPos, FHighLimit, DataLength);
    end;
  end;
  LastLineReached := (iPos >= FHighLimit);
  if LastLineReached and (FViewerControlMode in [vcmText, vcmWrap, vcmBook]) then
  begin
    if (GetPrevCharAsAscii(FHighLimit, CharLenInBytes) = 10) then
      Inc(Result);
  end;
end;

procedure TViewerControl.SetShowCaret(AValue: Boolean);
begin
  if FShowCaret <> AValue then
  begin
    FShowCaret:= AValue;

    if HandleAllocated then
      DoShowCaret;
  end;
end;

procedure TViewerControl.DoShowCaret;
begin
  if FShowCaret then
  begin
    LCLIntf.CreateCaret(Handle, 0, 2, FTextHeight);
    LCLIntf.ShowCaret(Handle);
    FCaretVisible:= True;
    Invalidate;
  end
  else begin
    FCaretVisible:= False;
    LCLIntf.HideCaret(Handle);
    LCLIntf.DestroyCaret(Handle);
  end;
end;

procedure TViewerControl.SetCaretPos(AValue: PtrInt);
begin
  if FCaretPos <> AValue then
  begin
    FCaretPos := AValue;
    if FShowCaret then Invalidate;
  end;
end;

function TViewerControl.GetPercent: Integer;
begin
  if FHighLimit - FLowLimit > 0 then
    Result := (Int64(FPosition - FLowLimit) * 100) div Int64(FHighLimit - FLowLimit)
  else
    Result := 0;
end;

procedure TViewerControl.SetPercent(const AValue: Integer);
begin
  if FHighLimit - FLowLimit > 0 then
    Position := Int64(AValue) * (Int64(FHighLimit  - FLowLimit) div 100) + FLowLimit
  else
    Position := 0;
end;

procedure TViewerControl.SetBlockBegin(const AValue: PtrInt);
begin
  if (AValue >= FLowLimit) and (AValue < FHighLimit) then
  begin
    if FBlockEnd < AValue then
      FBlockEnd := AValue;
    FBlockBeg   := AValue;
    Invalidate;
  end;
end;

procedure TViewerControl.SetBlockEnd(const AValue: PtrInt);
begin
  if (AValue >= FLowLimit) and (AValue < FHighLimit) then
  begin
    if FBlockBeg > AValue then
      FBlockBeg := AValue;
    FBlockEnd   := AValue;
    Invalidate;
  end;
end;

procedure TViewerControl.OutText(x, y: Integer; const sText: String;
  StartPos: PtrInt; DataLength: Integer);
var
  pBegLine, pEndLine: PtrInt;
  iBegDrawIndex, iEndDrawIndex: PtrInt;
begin
  pBegLine := StartPos;
  pEndLine := pBegLine + DataLength;

  Canvas.Font.Color := Font.Color;

  if FShowCaret and (FCaretPos >= pBegLine) and (FCaretPos <= pEndLine) then
  begin
    FCaretPoint.Y:= Y;
    FCaretPoint.X:= X + Canvas.TextWidth(GetText(StartPos, FCaretPos - pBegLine, 0));
  end;

  // Out of selection, draw normal
  if ((FBlockEnd - FBlockBeg) = 0) or ((FBlockBeg < pBegLine) and (FBlockEnd < pBegLine)) or // before
     ((FBlockBeg > pEndLine) and (FBlockEnd > pEndLine)) then // after
  begin
    Canvas.TextOut(x, y, sText);
    Exit;
  end;

  // Get selection start
  if (FBlockBeg <= pBegLine) then
    iBegDrawIndex := pBegLine
  else
    iBegDrawIndex := FBlockBeg;
  // Get selection end
  if (FBlockEnd < pEndLine) then
    iEndDrawIndex := FBlockEnd
  else
    iEndDrawIndex := pEndLine;

  // Text after selection.
  if pEndLine - iEndDrawIndex > 0 then
    Canvas.TextOut(x, y, sText);

  // Text before selection + selected text
  Canvas.Brush.Color := clHighlight;
  Canvas.Font.Color  := clHighlightText;

  Canvas.TextOut(X, Y, GetText(StartPos, iEndDrawIndex - pBegLine, 0));

  // Restore previous canvas settings
  Canvas.Brush.Color := Color;
  Canvas.Font.Color  := Font.Color;

  // Text before selection
  if iBegDrawIndex - pBegLine > 0 then
    Canvas.TextOut(X, Y, GetText(StartPos, iBegDrawIndex - pBegLine, 0));
end;

procedure TViewerControl.OutCustom(x, y: Integer; const sText: String;
  StartPos: PtrInt; DataLength: Integer);
var
  sTmpText: String;
  pBegLine, pEndLine: PtrInt;
  iBegDrawIndex, iEndDrawIndex: PtrInt;
begin
  pBegLine := StartPos;
  pEndLine := pBegLine + DataLength;

  Canvas.Font.Color := Font.Color;

  if FShowCaret and (FCaretPos >= pBegLine) and (FCaretPos <= pEndLine) then
  begin
    FCaretPoint.Y:= Y;
    FCaretPoint.X:= X + Canvas.TextWidth(Copy(sText, 1, FCustom.StartAscii + (FCaretPos - pBegLine)));
  end;

  // Out of selection, draw normal
  if ((FBlockEnd - FBlockBeg) = 0) or ((FBlockBeg < pBegLine) and (FBlockEnd <= pBegLine)) or // before
     ((FBlockBeg > pEndLine) and (FBlockEnd > pEndLine)) then // after
  begin
    // Offset + hex part + space between hex and ascii
    sTmpText:= Copy(sText, 1, FCustom.EndOfs) + '  ';
    Canvas.TextOut(x, y, sTmpText);
    x := x + Canvas.TextWidth(sTmpText);
    // Ascii part
    sTmpText := Copy(sText, 1 + FCustom.StartAscii, MaxInt);
    Canvas.TextOut(x, y, sTmpText);
    Exit;
  end;

  // Get selection start
  if (FBlockBeg <= pBegLine) then
    iBegDrawIndex := pBegLine
  else begin
    iBegDrawIndex := FBlockBeg;
  end;

  // Get selection end
  if (FBlockEnd < pEndLine) then
    iEndDrawIndex := FBlockEnd
  else begin
    iEndDrawIndex := pEndLine;
  end;

  // Text after selection (hex part)
  if pEndLine - iEndDrawIndex > 0 then
  begin
    sTmpText := Copy(sText, 1, FCustom.StartOfs + (pEndLine - pBegLine) * (FCustom.MaxValueDigits + FCustom.SpaceCount));
    Canvas.TextOut(x, y, sTmpText);
  end;

  // Text before selection + selected text (hex part)
  sTmpText := Copy(sText, 1, FCustom.StartOfs + (iEndDrawIndex - pBegLine) * (FCustom.MaxValueDigits + FCustom.SpaceCount) - 1);
  Canvas.Brush.Color := clHighlight;
  Canvas.Font.Color  := clHighlightText;
  Canvas.TextOut(x, y, sTmpText);

  // Restore previous canvas settings
  Canvas.Brush.Color := Color;
  Canvas.Font.Color  := Font.Color;

  // Offset + text before selection (hex part)
  sTmpText := Copy(sText, 1, FCustom.StartOfs + (iBegDrawIndex - pBegLine) * (FCustom.MaxValueDigits + FCustom.SpaceCount));
  Canvas.TextOut(x, y, sTmpText);

  // Offset + hex part + space between hex and ascii
  sTmpText:= Copy(sText, 1, FCustom.EndOfs) + '  ';
  x := x + Canvas.TextWidth(sTmpText);

  // Text after selection (ascii part)
  if pEndLine - iEndDrawIndex > 0 then
  begin
    sTmpText := Copy(sText, FCustom.StartAscii + 1, MaxInt);
    Canvas.TextOut(x, y, sTmpText);
  end;

  // Text before selection + selected text (ascii part)
  if (iEndDrawIndex - pBegLine) = FCustom.ValuesPerLine then
    sTmpText := Copy(sText, 1 + FCustom.StartAscii, MaxInt)
  else begin
    sTmpText := UTF8Copy(sText, 1 + FCustom.StartAscii, iEndDrawIndex - pBegLine);
  end;
  Canvas.Brush.Color := clHighlight;
  Canvas.Font.Color  := clHighlightText;
  Canvas.TextOut(x, y, sTmpText);

  // Restore background color
  Canvas.Brush.Color := Color;
  Canvas.Font.Color  := Font.Color;

  // Text before selection (ascii part)
  if iBegDrawIndex - pBegLine > 0 then
  begin
    sTmpText := UTF8Copy(sText, 1 + FCustom.StartAscii, iBegDrawIndex - pBegLine);
    Canvas.TextOut(x, y, sTmpText);
  end;
end;

procedure TViewerControl.OutBin(x, y: Integer; const sText: String;
  StartPos: PtrInt; DataLength: Integer);
var
  pBegLine, pEndLine: PtrInt;
  iBegDrawIndex, iEndDrawIndex: PtrInt;
begin
  pBegLine := StartPos;
  pEndLine := pBegLine + DataLength;

  Canvas.Font.Color := Font.Color;

  if FShowCaret and (FCaretPos >= pBegLine) and (FCaretPos <= pEndLine) then
  begin
    FCaretPoint.Y:= Y;
    FCaretPoint.X:= X + Canvas.TextWidth(Copy(sText, 1, FCaretPos - pBegLine));
  end;

  // Out of selection, draw normal
  if ((FBlockEnd - FBlockBeg) = 0) or ((FBlockBeg < pBegLine) and (FBlockEnd < pBegLine)) or // before
     ((FBlockBeg > pEndLine) and (FBlockEnd > pEndLine)) then //after
  begin
    Canvas.TextOut(x, y, sText);
    Exit;
  end;

  // Get selection start/end.
  if (FBlockBeg <= pBegLine) then
    iBegDrawIndex := pBegLine
  else begin
    iBegDrawIndex := FBlockBeg;
  end;

  if (FBlockEnd < pEndLine) then
    iEndDrawIndex := FBlockEnd
  else begin
    iEndDrawIndex := pEndLine;
  end;

  // Text after selection.
  if pEndLine - iEndDrawIndex > 0 then
    Canvas.TextOut(x, y, sText);

  // Text before selection + selected text
  Canvas.Brush.Color := clHighlight;
  Canvas.Font.Color  := clHighlightText;

  // Whole line selected
  if (iEndDrawIndex - pBegLine) = DataLength then
    Canvas.TextOut(X, Y, sText)
  else begin
    Canvas.TextOut(X, Y, UTF8Copy(sText, 1, iEndDrawIndex - pBegLine));
  end;

  // Restore previous canvas settings
  Canvas.Brush.Color := Color;
  Canvas.Font.Color  := Font.Color;

  // Text before selection
  if iBegDrawIndex - pBegLine > 0 then
    Canvas.TextOut(X, Y, UTF8Copy(sText, 1, iBegDrawIndex - pBegLine));
end;

procedure TViewerControl.AddLineOffset(const iOffset: PtrInt);
begin
  FLineList.Add(iOffset);
end;

procedure TViewerControl.KeyDown(var Key: word; Shift: TShiftState);
var
  CharLenInBytes: Integer;
begin
  if Shift = [] then
  begin
    case Key of
      VK_DOWN:
        begin
          Key := 0;
          Scroll(1);
        end;
      VK_UP:
        begin
          Key := 0;
          Scroll(-1);
        end;
      VK_RIGHT:
        begin
          Key := 0;
          HScroll(1);
        end;
      VK_LEFT:
        begin
          Key := 0;
          HScroll(-1);
        end;
      VK_HOME:
        begin
          Key := 0;
          CaretGoHome;
        end;
      VK_END:
        begin
          Key := 0;
          CaretGoEnd;
        end;
      VK_PRIOR:
        begin
          Key := 0;
          PageUp;
        end;
      VK_NEXT:
        begin
          Key := 0;
          PageDown;
        end;
      else
        inherited KeyDown(Key, Shift);
    end;
  end
  else if Shift = [ssCtrl] then
  begin
    case Key of
      VK_HOME:
        begin
          Key := 0;
          CaretPos := FLowLimit;
          MakeVisible(FCaretPos)
        end;
      VK_END:
        begin
          Key := 0;
          CaretPos := FHighLimit;
          MakeVisible(FCaretPos);
        end;
      else
        inherited KeyDown(Key, Shift);
    end;
  end
  else
    inherited KeyDown(Key, Shift);
end;

function TViewerControl.FindAsciiSetForward(aPosition, aMaxBytes: PtrInt;
                                            const AsciiChars: String;
                                            bFindNotIn: Boolean): PtrInt;
var
  i: Integer;
  found: Boolean;
  u: Cardinal;
  CharLenInBytes: Integer;
begin
  Result := -1;
  while aMaxBytes > 0 do
  begin
    u := GetNextCharAsAscii(aPosition, CharLenInBytes);
    if CharLenInBytes = 0 then
      Exit;

    if not bFindNotIn then
    begin
      for i := 1 to Length(AsciiChars) do
        if u = ord(AsciiChars[i]) then
          Exit(aPosition);
    end
    else
    begin
      found := False;
      for i := 1 to Length(AsciiChars) do
        if u = ord(AsciiChars[i]) then
        begin
          found := True;
          break;
        end;

      if not found then
        Exit(aPosition);
    end;

    Inc(aPosition, CharLenInBytes);
    Dec(aMaxBytes, CharLenInBytes);
  end;
end;

function TViewerControl.FindAsciiSetBackward(aPosition, aMaxBytes: PtrInt;
                                             const AsciiChars: String;
                                             bFindNotIn: Boolean): PtrInt;
var
  i: Integer;
  found: Boolean;
  u: Cardinal;
  CharLenInBytes: Integer;
begin
  Result := -1;
  while aMaxBytes > 0 do
  begin
    u := GetPrevCharAsAscii(aPosition, CharLenInBytes);
    if CharLenInBytes = 0 then
      Exit;

    if not bFindNotIn then
    begin
      for i := 1 to Length(AsciiChars) do
        if u = ord(AsciiChars[i]) then
          Exit(aPosition);
    end
    else
    begin
      found := False;
      for i := 1 to Length(AsciiChars) do
        if u = ord(AsciiChars[i]) then
        begin
          found := True;
          break;
        end;

      if not found then
        Exit(aPosition);
    end;

    Dec(aPosition, CharLenInBytes);
    Dec(aMaxBytes, CharLenInBytes);
  end;
end;

procedure TViewerControl.MouseDown(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
var
  LineBegin, LineEnd: PtrInt;
  ClickPos: PtrInt;
  CharSide: TCharSide;
  CharLenInBytes: Integer;
begin
  inherited;

  SetFocus;

  if not IsFileOpen then
    Exit;

  case Button of
    mbLeft:
      begin
        if Shift * [ssDouble, ssTriple] = [] then
        begin
          // Single click.
          ClickPos := XYPos2Adr(x, y, CharSide);
          if ClickPos <> -1 then
          begin
            FBlockBeg       := ClickPos;
            FBlockEnd       := ClickPos;
            FCaretPos       := ClickPos;
            FMouseBlockBeg  := ClickPos;
            FMouseBlockSide := CharSide;
            FSelecting      := True;

            if CharSide in [csRight, csAfter] then
            begin
              if FViewerControlMode in [vcmDec, vcmHex, vcmBin] then
                CharLenInBytes := 1
              else begin
                GetNextCharAsAscii(FCaretPos, CharLenInBytes);
              end;
              FCaretPos := FCaretPos + CharLenInBytes;
            end;

            Invalidate;
          end
          else
            FSelecting := False;
        end
        else // if double click or triple click
        begin
          FSelecting := False;
          LineBegin  := GetStartOfLine(FMouseBlockBeg);
          LineEnd    := GetEndOfLine(FMouseBlockBeg);

          if ssDouble in Shift then
          begin
            // Select word with double-click.
            FBlockBeg := FindAsciiSetBackward(FMouseBlockBeg,
                           FMouseBlockBeg - LineBegin, sNonCharacter, False);

            FBlockEnd := FindAsciiSetForward(FMouseBlockBeg,
                           LineEnd - FMouseBlockBeg, sNonCharacter, False);
          end
          else if ssTriple in Shift then
          begin
            // Select line with triple-click.
            FBlockBeg := FindAsciiSetForward(LineBegin,
                           LineEnd - LineBegin, sWhiteSpace, True);

            FBlockEnd := FindAsciiSetBackward(LineEnd,
                           LineEnd - LineBegin, sWhiteSpace, True);
          end;

          if FBlockBeg = -1 then
            FBlockBeg := LineBegin;
          if FBlockEnd = -1 then
            FBlockEnd := LineEnd;

          if FBlockBeg > FBlockEnd then
            FBlockEnd := FBlockBeg;

          if FAutoCopy then
            CopyToClipboard;
          Invalidate;
        end;
      end; // mbLeft
  end; // case
end;

procedure TViewerControl.MouseMove(Shift: TShiftState; X, Y: Integer);

  procedure MoveOneChar(var aPosition: PtrInt);
  var
    CharLenInBytes: Integer;
  begin
    if FViewerControlMode in [vcmDec, vcmHex, vcmBin] then
      CharLenInBytes := 1
    else begin
      GetNextCharAsAscii(aPosition, CharLenInBytes);
    end;
    aPosition := aPosition + CharLenInBytes;
  end;

  procedure MoveOneCharByMouseSide(var aPosition: PtrInt);
  begin
    if FMouseBlockSide in [csRight, csAfter] then
      MoveOneChar(aPosition);
  end;

var
  ClickPos: PtrInt;
  CharSide: TCharSide;
begin
  inherited;

  if FSelecting then
  begin
    if y < FTextHeight then
      Scroll(-3)
    else if y > ClientHeight - FTextHeight then
      Scroll(3);

    ClickPos := XYPos2Adr(x, y, CharSide);
    if ClickPos <> -1 then
    begin
      if ClickPos < FMouseBlockBeg then
      begin
        // Got a new beginning.
        FBlockBeg := ClickPos;
        FBlockEnd := FMouseBlockBeg;

        // Move end beyond last character.
        MoveOneCharByMouseSide(FBlockEnd);

        // When selecting from right to left, the current selected side must be
        // either csLeft or csBefore, otherwise current position is not included.
        if not (CharSide in [csLeft, csBefore]) then
        begin
          // Current position should not be included in selection.
          // Move beginning after first character.
          MoveOneChar(FBlockBeg);
        end;
        FCaretPos:= FBlockBeg;
      end
      else if ClickPos > FMouseBlockBeg then
      begin
        // Got a new end.
        FBlockBeg := FMouseBlockBeg;
        FBlockEnd := ClickPos;

        // Move beginning after first character.
        MoveOneCharByMouseSide(FBlockBeg);

        // When selecting from left to right, the current selected side must be
        // either csRight or csAfter, otherwise current position is not included.
        if CharSide in [csRight, csAfter] then
        begin
          // Current position should be included in selection.
          // Move end beyond last character.
          MoveOneChar(FBlockEnd);
        end;
        FCaretPos:= FBlockEnd;
      end
      else if FMouseBlockSide <> CharSide then
      begin
        // Same position but changed side of the character.
        FBlockBeg := FMouseBlockBeg;
        FBlockEnd := FMouseBlockBeg;

        if ((FMouseBlockSide in [csBefore, csLeft]) and
            (CharSide in [csRight, csAfter])) or
           ((FMouseBlockSide in [csRight, csAfter]) and
            (CharSide in [csBefore, csLeft])) then
        begin
          // Move end beyond last character.
          MoveOneChar(FBlockEnd);
        end;
        FCaretPos:= FBlockEnd;
      end
      else
      begin
        FBlockBeg := FMouseBlockBeg;
        FBlockEnd := FMouseBlockBeg;
      end;

      Invalidate;
    end;
  end;
end;

procedure TViewerControl.MouseUp(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
begin
  inherited;

  if FSelecting and (Button = mbLeft) and (Shift * [ssDouble, ssTriple] = []) then
  begin
    if FAutoCopy then
      CopyToClipboard;
    FSelecting := False;
  end;
end;

function TViewerControl.DoMouseWheelDown(Shift: TShiftState; MousePos: TPoint): Boolean;
begin
  Result := inherited;
  if not Result then
    Result := Scroll(Mouse.WheelScrollLines);
end;

function TViewerControl.DoMouseWheelUp(Shift: TShiftState; MousePos: TPoint): Boolean;
begin
  Result := inherited;
  if not Result then
    Result := Scroll(-Mouse.WheelScrollLines);
end;

function TViewerControl.DoMouseWheelLeft(Shift: TShiftState; MousePos: TPoint): Boolean;
begin
  Result:= inherited DoMouseWheelLeft(Shift, MousePos);
  if not Result then
    Result := HScroll(-Mouse.WheelScrollLines);
end;

function TViewerControl.DoMouseWheelRight(Shift: TShiftState; MousePos: TPoint): Boolean;
begin
  Result:= inherited DoMouseWheelRight(Shift, MousePos);
  if not Result then
    Result := HScroll(Mouse.WheelScrollLines);
end;

function TViewerControl.XYPos2Adr(x, y: Integer; out CharSide: TCharSide): PtrInt;
var
  yIndex:  Integer;
  StartLine, EndLine: PtrInt;

  function XYPos2AdrBin: PtrInt;
  var
    I, J, L:  Integer;
    charWidth: Integer;
    textWidth: Integer;
    tmpPosition: PtrInt;
    s, ss, sText: String;
    InvalidCharLen: Integer;
  begin
    J:= 1;
    ss := EmptyStr;
    tmpPosition := StartLine;
    sText := TransformBin(tmpPosition, EndLine);
    L:= Length(sText);

    for I := 1 to L do
    begin
      charWidth:= SafeUTF8NextCharLen(PByte(@sText[J]), (L - J) + 1, InvalidCharLen);
      s:= Copy(sText, J, charWidth);
      Inc(J, charWidth);
      ss := ss + s;
      textWidth := Canvas.TextWidth(ss);
      if textWidth > x then
      begin
        charWidth := Canvas.TextWidth(s);
        if textWidth - charWidth div 2 > x then
          CharSide := csLeft
        else
          CharSide := csRight;

        Exit(StartLine + I - 1);  // -1 because we count from 1
      end;
    end;
    CharSide := csBefore;
    Result := EndLine;
  end;


  function XYPos2AdrCustom: PtrInt;
  //    | offset part  | custom part  | native part |
  //    |     0000AAAA: | FF AA CC AE | djfjks       |

  var
    I, J, L:  Integer;
    charWidth: Integer;
    textWidth: Integer;
    tmpPosition: PtrInt;
    InvalidCharLen: Integer;
    ss, sText, sPartialText: String;
  begin
    tmpPosition  := StartLine;
    sText := TransformCustom(tmpPosition, EndLine);
    if sText = '' then Exit;

    // Clicked on offset part
    ss := Copy(sText, 1, FCustom.StartOfs);
    textWidth := Canvas.TextWidth(ss);
    if textWidth > x then
    begin
      CharSide := csBefore;
      Exit(StartLine);
    end;

    // Clicked on custom part
    for I := 0 to FCustom.ValuesPerLine - 1 do
    begin
      sPartialText := Copy(sText, 1 + FCustom.StartOfs + I * (FCustom.MaxValueDigits + FCustom.SpaceCount), FCustom.MaxValueDigits);
      ss := ss + sPartialText;
      textWidth := Canvas.TextWidth(ss);
      if textWidth > x then
      begin
        // Check if we're not after end of data.
        if StartLine + I >= EndLine then
        begin
          CharSide := csBefore;
          Exit(EndLine);
        end;

        charWidth := Canvas.TextWidth(sPartialText);

        if textWidth - charWidth div 2 > x then
          CharSide := csLeft
        else
          CharSide := csRight;

        Exit(StartLine + I);
      end;

      // Space after hex number.
      ss := ss + string(sText[1 + FCustom.StartOfs + I * (FCustom.MaxValueDigits + 1) + FCustom.MaxValueDigits]);
      textWidth := Canvas.TextWidth(ss);

      if textWidth > x then
      begin
        CharSide := csAfter;
        Exit(StartLine + I);
      end;
    end;

    // Clicked between hex and ascii.
    sPartialText := Copy(sText, 1 + FCustom.StartOfs, FCustom.StartAscii - FCustom.EndOfs);
    ss := ss + sPartialText;
    textWidth := Canvas.TextWidth(ss);
    if textWidth > x then
    begin
      Exit(-1); // No position.
    end;

    // Clicked on ascii part.
    L:= Length(sText);
    J:= 1 + FCustom.StartAscii;
    for I := 0 to FCustom.ValuesPerLine - 1 do
    begin
      charWidth := SafeUTF8NextCharLen(PByte(@sText[J]), (L - J) + 1, InvalidCharLen);
      sPartialText := Copy(sText, J, charWidth);
      Inc(J, charWidth);
      ss := ss + sPartialText;
      textWidth := Canvas.TextWidth(ss);
      if textWidth > x then
      begin
        // Check if we're not after end of data.
        if StartLine + I >= EndLine then
        begin
          CharSide := csBefore;
          Exit(EndLine);
        end;

        charWidth := Canvas.TextWidth(sPartialText);

        if textWidth - charWidth div 2 > x then
          CharSide := csLeft
        else
          CharSide := csRight;

        Exit(StartLine + I);
      end;
    end;

    CharSide := csBefore;
    Result := EndLine;
  end;

  function XYPos2AdrText: PtrInt;
  var
    J: Integer;
    Dos: Boolean;
    s, ss: String;
    Len: Integer = 0;
    charWidth: Integer;
    textWidth: Integer;
    tmpPosition: PtrInt;
    CharLenInBytes: Integer;
  begin
    ss := '';
    tmpPosition := StartLine;

    if (Mode = vcmText) and (FHPosition > 0) then
    begin
      for J:= 1 to FHPosition do
      begin
        GetNextCharAsAscii(tmpPosition, CharLenInBytes);
        tmpPosition += CharLenInBytes;
      end;
    end;

    Dos:= FEncoding in ViewerEncodingOem;
    while tmpPosition < EndLine do
    begin
      s := GetNextCharAsUtf8(tmpPosition, CharLenInBytes);
      if CharLenInBytes = 0 then
        Break;

      // Check if the conversion to UTF-8 was successful.
      if Length(s) > 0 then
      begin
        if s = #9 then
        begin
          s := StringOfChar(' ', FTabSpaces - Len mod FTabSpaces);
          Len := Len + (FTabSpaces - Len mod FTabSpaces);
        end
        else
          Inc(Len); // Assume there is one character after conversion
                    // (otherwise use Inc(Len, UTF8Length(s))).

        if (CharLenInBytes = 1) and (s[1] < ' ') then
        begin
          if Dos then
            s := ASCII_TABLE[Ord(s[1])]
          else
            s := ' ';
        end;

        ss := ss + s;
        textWidth := Canvas.TextWidth(ss);
        if textWidth > x then
        begin
          charWidth := Canvas.TextWidth(s);
          if textWidth - charWidth div 2 > x then
            CharSide := csLeft
          else
            CharSide := csRight;

          Exit(tmpPosition);
        end;
      end;

      tmpPosition := tmpPosition + CharLenInBytes;
    end;

    CharSide := csBefore;
    Result := EndLine;
  end;

begin
  if FLineList.Count = 0 then
    Exit(-1);

  if (x < FLeftMargin) then
    x := 0
  else begin
    x := x - FLeftMargin;
  end;

  yIndex := y div FTextHeight;
  if yIndex >= FLineList.Count then
    yIndex := FLineList.Count - 1;
  if yIndex < 0 then
    yIndex := 0;

  // Get position of first character of the line.
  StartLine := FLineList.Items[yIndex];
  // Get position of last character of the line.
  EndLine   := GetEndOfLine(StartLine);

  if (x = 0) and ((Mode <> vcmText) or (FHPosition = 0)) then
  begin
    CharSide := csBefore;
    Exit(StartLine);
  end;

  case Mode of
    vcmBin:
      Result := XYPos2AdrBin;
    vcmHex,vcmDec:
      Result := XYPos2AdrCustom;  // XYPos2AdrHex;
    vcmText, vcmWrap, vcmBook:
      Result := XYPos2AdrText;
    else
      raise Exception.Create('Invalid viewer mode');
  end;

end;

procedure TViewerControl.SelectAll;
begin
  SelectText(FLowLimit, FHighLimit);
end;

procedure TViewerControl.SelectText(AStart, AEnd: PtrInt);
begin
  if AStart < FLowLimit then
    AStart := FLowLimit;
  if AEnd > FHighLimit then
    AEnd := FHighLimit;

  if AStart <= AEnd then
  begin
    FBlockBeg := AStart;
    FBlockEnd := AEnd;
    Invalidate;
  end;
end;

procedure TViewerControl.CopyToClipboard;
var
  sText, utf8Text: string;
begin
  if (FBlockEnd - FBlockBeg) <= 0 then
    Exit;
  if (FBlockEnd - FBlockBeg) > 1024 * 1024 then // Max 1 MB to clipboard
    Exit;
  SetString(sText, GetDataAdr + FBlockBeg, FBlockEnd - FBlockBeg);
  utf8Text := ConvertToUTF8(sText);
  {$IFDEF LCLGTK2}
  // Workaround for Lazarus bug #0021453. LCL adds trailing zero to clipboard in Clipboard.AsText.
  Clipboard.Clear;
  Clipboard.AddFormat(PredefinedClipboardFormat(pcfText), utf8Text[1], Length(utf8Text));
  {$ELSE}
  Clipboard.AsText := utf8Text;
  {$ENDIF}
end;

procedure TViewerControl.CopyToClipboardF;
var
  s,sText, utf8Text: string;
  len: Integer;
begin
   len:=FBlockEnd-FBlockBeg;
   if len=0 then exit;
   sText:=TransformCustomBlock(FBlockBeg,len,False,False,s);

   utf8Text := ConvertToUTF8(sText);
   {$IFDEF LCLGTK2}
   // Workaround for Lazarus bug #0021453. LCL adds trailing zero to clipboard in Clipboard.AsText.
   Clipboard.Clear;
   Clipboard.AddFormat(PredefinedClipboardFormat(pcfText), utf8Text[1], Length(utf8Text));
   {$ELSE}
   Clipboard.AsText := utf8Text;
   {$ENDIF}
end;

function TViewerControl.Selection: String;
const
  MAX_LEN = 512;
var
  sText: String;
  AIndex: PtrInt;
  ALength: PtrInt;
  CharLenInBytes: Integer;
begin
  if (FBlockEnd - FBlockBeg) <= 0 then
    Exit(EmptyStr);
  ALength:= FBlockEnd - FBlockBeg;
  if ALength <= MAX_LEN then
  begin
    SetString(sText, GetDataAdr + FBlockBeg, ALength);
    Result := ConvertToUTF8(sText);
  end
  else begin
    Result:= EmptyStr;
    AIndex:= FBlockBeg;
    ALength:= AIndex + MAX_LEN;
    while AIndex < ALength do
    begin
      sText := GetNextCharAsUtf8(AIndex, CharLenInBytes);
      if CharLenInBytes = 0 then
        Break;
      Result:= Result + sText;
      AIndex:= AIndex + CharLenInBytes;
    end;
  end;
end;

function TViewerControl.GetNextCharAsAscii(const iPosition: PtrInt; out CharLenInBytes: Integer): Cardinal;
var
  u1, u2: Word;
  InvalidCharLen: Integer;
begin
  Result := 0;
  case FEncoding of
    veUtf8, veUtf8bom:
      begin
        if iPosition < FHighLimit then
        begin
          CharLenInBytes := SafeUTF8NextCharLen(GetDataAdr + iPosition,
                                                FHighLimit - iPosition,
                                                InvalidCharLen);

          // It's enough to only return Ascii.
          if CharLenInBytes = 1 then
            Result := PByte(GetDataAdr)[iPosition];

          // Full conversion:
          // Result := UTF8CodepointToUnicode(PAnsiChar(GetDataAdr + iPosition), CharLenInBytes);
        end
        else
          CharLenInBytes := 0;
      end;

    veAnsi, veOem,
    veCp1250..veCp874,
    veIso88591,
    veIso88592,
    veKoi8r,
    veKoi8u,
    veKoi8ru:
      if iPosition < FHighLimit then
      begin
        Result := PByte(GetDataAdr)[iPosition];
        CharLenInBytes := 1;
      end
      else
        CharLenInBytes := 0;

    veUcs2be:
      if iPosition + SizeOf(Word) - 1 < FHighLimit then
      begin
        Result := BEtoN(PWord(GetDataAdr + iPosition)[0]);
        CharLenInBytes := SizeOf(Word);
      end
      else
        CharLenInBytes := 0;

    veUcs2le:
      if iPosition + SizeOf(Word) - 1 < FHighLimit then
      begin
        Result := LEtoN(PWord(GetDataAdr + iPosition)[0]);
        CharLenInBytes := SizeOf(Word);
      end
      else
        CharLenInBytes := 0;

    veUtf16be:
      if iPosition + SizeOf(Word) - 1 < FHighLimit then
      begin
        u1 := BEtoN(PWord(GetDataAdr + iPosition)[0]);
        CharLenInBytes := UTF16CharacterLength(@u1);
        if CharLenInBytes = 1 then
        begin
          Result := u1;
        end
        else if iPosition + SizeOf(Word) * CharLenInBytes - 1 < FHighLimit then
        begin
          u2 := BEtoN(PWord(GetDataAdr + iPosition)[1]);
          Result := utf16PairToUnicode(u1, u2);
        end;

        CharLenInBytes := CharLenInBytes * SizeOf(Word);
      end
      else
        CharLenInBytes := 0;

    veUtf16le:
      if iPosition + SizeOf(Word) - 1 < FHighLimit then
      begin
        u1 := LEtoN(PWord(GetDataAdr + iPosition)[0]);
        CharLenInBytes := UTF16CharacterLength(@u1);
        if CharLenInBytes = 1 then
        begin
          Result := u1;
        end
        else if iPosition + SizeOf(Word) * CharLenInBytes - 1 < FHighLimit then
        begin
          u2 := LEtoN(PWord(GetDataAdr + iPosition)[1]);
          Result := utf16PairToUnicode(u1, u2);
        end
        else
          CharLenInBytes := 0;

        CharLenInBytes := CharLenInBytes * SizeOf(Word);
      end
      else
        CharLenInBytes := 0;

    veUtf32be:
      if iPosition + SizeOf(LongWord) - 1 < FHighLimit then
      begin
        Result := BEtoN(PLongWord(GetDataAdr + iPosition)[0]);
        CharLenInBytes := SizeOf(LongWord);
      end
      else
        CharLenInBytes := 0;

    veUtf32le:
      if iPosition + SizeOf(LongWord) - 1 < FHighLimit then
      begin
        Result := LEtoN(PLongWord(GetDataAdr + iPosition)[0]);
        CharLenInBytes := SizeOf(LongWord);
      end
      else
        CharLenInBytes := 0;

    veCp932, // Unsupported variable-width encodings
    veCp936, // TODO: Add cp932, cp936, cp949, cp950 encoding support
    veCp949,
    veCp950:
      if iPosition < FHighLimit then
      begin
        Result := PByte(GetDataAdr)[iPosition];
        CharLenInBytes := 1;
      end
      else
        CharLenInBytes := 0;

    else
      raise Exception.Create('Unsupported viewer encoding');
  end;
end;

function TViewerControl.GetPrevCharAsAscii(const iPosition: PtrInt; out CharLenInBytes: Integer): Cardinal;
var
  u1, u2: Word;
  InvalidCharLen: Integer;
begin
  Result := 0;
  case FEncoding of
    veUtf8, veUtf8bom:
      begin
        if iPosition > FLowLimit then
        begin
          CharLenInBytes := SafeUTF8PrevCharLen(GetDataAdr + iPosition,
                                                iPosition - FLowLimit,
                                                InvalidCharLen);

          // It's enough to only return Ascii.
          if CharLenInBytes = 1 then
            Result := PByte(GetDataAdr)[iPosition - 1];

          // Full conversion:
          // Result := UTF8CodepointToUnicode(PAnsiChar(GetDataAdr + iPosition - CharLenInBytes), CharLenInBytes);
        end
        else
          CharLenInBytes := 0;
      end;

    veAnsi, veOem,
    veCp1250..veCp874,
    veIso88591,
    veIso88592,
    veKoi8r,
    veKoi8u,
    veKoi8ru:
      if iPosition > FLowLimit then
      begin
        Result := PByte(GetDataAdr + iPosition)[-1];
        CharLenInBytes := 1;
      end
      else
        CharLenInBytes := 0;

    veUcs2be:
      if iPosition >= FLowLimit + SizeOf(Word) then
      begin
        Result := BEtoN(PWord(GetDataAdr + iPosition)[-1]);
        CharLenInBytes := SizeOf(Word);
      end
      else
        CharLenInBytes := 0;

    veUcs2le:
      if iPosition >= FLowLimit + SizeOf(Word) then
      begin
        Result := LEtoN(PWord(GetDataAdr + iPosition)[-1]);
        CharLenInBytes := SizeOf(Word);
      end
      else
        CharLenInBytes := 0;

    veUtf16be:
      if iPosition >= FLowLimit + SizeOf(Word) then
      begin
        u1 := BEtoN(PWord(GetDataAdr + iPosition)[-1]);
        CharLenInBytes := UTF16CharacterLength(@u1);
        if CharLenInBytes = 1 then
        begin
          Result := u1;
        end
        else if iPosition >= FLowLimit + SizeOf(Word) * CharLenInBytes then
        begin
          u2 := BEtoN(PWord(GetDataAdr + iPosition)[-2]);
          // u2 is the first, u1 is the second value of the pair
          Result := utf16PairToUnicode(u2, u1);
        end;

        CharLenInBytes := CharLenInBytes * SizeOf(Word);
      end
      else
        CharLenInBytes := 0;

    veUtf16le:
      if iPosition >= FLowLimit + SizeOf(Word) then
      begin
        u1 := LEtoN(PWord(GetDataAdr + iPosition)[-1]);
        CharLenInBytes := UTF16CharacterLength(@u1);
        if CharLenInBytes = 1 then
        begin
          Result := u1;
        end
        else if iPosition >= FLowLimit + SizeOf(Word) * CharLenInBytes then
        begin
          u2 := LEtoN(PWord(GetDataAdr + iPosition)[-2]);
          // u2 is the first, u1 is the second value of the pair
          Result := utf16PairToUnicode(u2, u1);
        end;

        CharLenInBytes := CharLenInBytes * SizeOf(Word);
      end
      else
        CharLenInBytes := 0;

    veUtf32be:
      if iPosition >= FLowLimit + SizeOf(LongWord) then
      begin
        Result := BEtoN(PLongWord(GetDataAdr + iPosition)[-1]);
        CharLenInBytes := SizeOf(LongWord);
      end
      else
        CharLenInBytes := 0;

    veUtf32le:
      if iPosition >= FLowLimit + SizeOf(LongWord) then
      begin
        Result := LEtoN(PLongWord(GetDataAdr + iPosition)[-1]);
        CharLenInBytes := SizeOf(LongWord);
      end
      else
        CharLenInBytes := 0;

    veCp932, // Unsupported variable-width encodings
    veCp936, // TODO: Add cp932, cp936, cp949, cp950 encoding support
    veCp949,
    veCp950:
      if iPosition > FLowLimit then
      begin
        Result := PByte(GetDataAdr + iPosition)[-1];
        CharLenInBytes := 1;
      end
      else
        CharLenInBytes := 0;

    else
      raise Exception.Create('Unsupported viewer encoding');
  end;
end;

function TViewerControl.GetNextCharAsUtf8(const iPosition: PtrInt; out CharLenInBytes: Integer): String;
var
  u1: Word;
  s: string;
  InvalidCharLen: Integer;
begin
  Result := '';
  case FEncoding of
    veUtf8, veUtf8bom:
      CharLenInBytes := SafeUTF8NextCharLen(GetDataAdr + iPosition,
                                            FHighLimit - iPosition,
                                            InvalidCharLen);
    veAnsi, veOem,
    veCp1250..veCp874,
    veIso88591,
    veIso88592,
    veKoi8r,
    veKoi8u,
    veKoi8ru:
      CharLenInBytes := 1;
    veUcs2be, veUcs2le:
      CharLenInBytes := 2;
    veUtf16be:
      if iPosition + SizeOf(Word) - 1 < FHighLimit then
      begin
        u1 := BEtoN(PWord(GetDataAdr + iPosition)[0]);
        CharLenInBytes := UTF16CharacterLength(@u1) * SizeOf(Word);
      end
      else
        CharLenInBytes := 0;
    veUtf16le:
      if iPosition + SizeOf(Word) - 1 < FHighLimit then
      begin
        u1 := LEtoN(PWord(GetDataAdr + iPosition)[0]);
        CharLenInBytes := UTF16CharacterLength(@u1) * SizeOf(Word);
      end
      else
        CharLenInBytes := 0;
    veUtf32be, veUtf32le:
      CharLenInBytes := 4;

    veCp932, // Unsupported variable-width encodings
    veCp936, // TODO: Add cp932, cp936, cp949, cp950 encoding support
    veCp949,
    veCp950:
      CharLenInBytes := 1;

    else
      raise Exception.Create('Unsupported viewer encoding');
  end;

  if (CharLenInBytes > 0) and (iPosition + CharLenInBytes - 1 < FHighLimit) then
  begin
    SetString(s, GetDataAdr + iPosition, CharLenInBytes);
    Result := ConvertToUTF8(s);
  end
  else
    Result := '';
end;

function TViewerControl.ConvertToUTF8(const sText: AnsiString): String;
begin
  if FEncoding = veAutoDetect then
    FEncoding := DetectEncoding;  // Force detect encoding.

  case FEncoding of
    veAutoDetect: ;
    veAnsi:
      Result := CeAnsiToUtf8(sText);
    veOem:
      Result := CeOemToUtf8(sText);
    veUtf8, veUtf8bom:
      Result := Utf8ReplaceBroken(sText);
    veUtf16be:
      Result := Utf16BEToUtf8(sText);
    veUtf16le:
      Result := Utf16LEToUtf8(sText);
    veUtf32be:
      Result := Utf32BEToUtf8(sText);
    veUtf32le:
      Result := Utf32LEToUtf8(sText);
    else
      Result := LConvEncoding.ConvertEncoding(sText,
                    ViewerEncodingsNames[FEncoding], EncodingUTF8);
  end;
end;

function TViewerControl.ConvertFromUTF8(const sText: String): AnsiString;
begin
  if FEncoding = veAutoDetect then
    FEncoding := DetectEncoding;  // Force detect encoding.

  case FEncoding of
    veAutoDetect: ;
    veAnsi:
      Result := CeUtf8ToAnsi(sText);
    veOem:
      Result := CeUtf8ToOem(sText);
    veUtf8, veUtf8bom:
      Result := sText;
    veUtf16be:
      Result := Utf8ToUtf16BE(sText);
    veUtf16le:
      Result := Utf8ToUtf16LE(sText);
    veUtf32be:
      Result := '';//Utf8ToUtf32BE(sText);
    veUtf32le:
      Result := '';//Utf8ToUtf32LE(sText);
    else
      Result := LConvEncoding.ConvertEncoding(sText,
                    EncodingUTF8, ViewerEncodingsNames[FEncoding]);
  end;
end;

function TViewerControl.IsVisible(const aPosition: PtrInt): Boolean;
var
  StartPos: PtrInt;
  CharLenInBytes: Integer;
begin
  if IsFileOpen and (FLineList.Count > 0) then
    begin
      FVisibleOffset:= 0;
      StartPos:= GetStartOfLine(aPosition);
      // Calculate horizontal offset in symbols
      while (StartPos < aPosition) do
      begin
        GetNextCharAsAscii(StartPos, CharLenInBytes);
        Inc(StartPos, CharLenInBytes);
        Inc(FVisibleOffset);
      end;

      Result := (aPosition >= FLineList.Items[0]) and
                (aPosition <= FLineList.Items[FLineList.Count - 1]) and
                (FVisibleOffset >= FHPosition) and
                (FVisibleOffset <= FHPosition + FTextWidth);
    end
  else
    Result := False;
end;

procedure TViewerControl.MakeVisible(const aPosition: PtrInt);
var
  Offset: Integer;
  LastLine: Boolean;
begin
  if not IsVisible(aPosition) then
  begin
    SetPosition(aPosition);
    Offset:= GetLinesTillEnd(aPosition, LastLine);
    if (Offset > 4) and (LastLine = False) then Scroll(-4);

    Update;

    if FViewerControlMode = vcmText then
    begin
      if (FVisibleOffset < FHPosition) or
         (FVisibleOffset > FHPosition + FTextWidth) then
      begin
        SetHPosition(FVisibleOffset);
        HScroll(-1);
      end;
    end;
  end;
end;

procedure TViewerControl.UpdateScrollbars;
var
  ScrollVisibleH: Boolean;
  ScrollInfo: TScrollInfo;
begin
  // ShowScrollBar may send a resize message
  // and trigger DoOnResize -> UpdateScrollbars
  if HandleAllocated and (FScrollBarsLock = 0) then
  begin
    Inc(FScrollBarsLock);
    try
      ScrollInfo:= Default(TScrollInfo);
      ScrollInfo.cbSize:= SizeOf(ScrollInfo);
      ScrollInfo.fMask:= SIF_POS or SIF_RANGE or SIF_PAGE or SIF_DISABLENOSCROLL;
      ScrollInfo.nPage:= 1;
      ScrollInfo.nMax:= 100;
      // Vertical
      ScrollInfo.nPos:= FScrollBarPosition;
      ShowScrollBar(Handle, SB_Vert, True);
      SetScrollInfo(Handle, SB_Vert, ScrollInfo, True);
      // Horizontal
      ScrollVisibleH:= (FViewerControlMode = vcmText);
      ShowScrollBar(Handle, SB_Horz, ScrollVisibleH);
      if ScrollVisibleH then
      begin
        ScrollInfo.nPos:= FHScrollBarPosition;
        SetScrollInfo(Handle, SB_Horz, ScrollInfo, ScrollVisibleH);
      end;
    finally
      Dec(FScrollBarsLock);
    end;
  end;
end;

procedure TViewerControl.DoOnResize;
begin
  inherited DoOnResize;
  UpdateScrollbars;
  // Force recalculating position.
  SetPosition(FPosition);
  SetHPosition(FHPosition);
end;

procedure TViewerControl.ReReadFile;
begin
  FBlockBeg := 0;
  FBlockEnd := 0;

  FBOMLength := GetBomLength;
  UpdateLimits;

  UpdateScrollbars;
  Invalidate;
end;

function TViewerControl.IsFileOpen: Boolean;
begin
  Result := Assigned(FMappedFile);
end;

function TViewerControl.DetectEncoding: TViewerEncoding;
var
  DetectStringLength: Integer = 4096; // take first 4kB of the file to detect encoding
  DetectString: String;
  DetectedEncodingName: String;
  Enc: TViewerEncoding;
begin
  if IsFileOpen then
  begin
    // Default to Ansi in case encoding cannot be detected or is unsupported.
    Result := veAnsi;

    if FFileSize < DetectStringLength then
      DetectStringLength := FFileSize;

    SetString(DetectString, PAnsiChar(FMappedFile), DetectStringLength);

    if Assigned(FOnGuessEncoding) then
      DetectedEncodingName := FOnGuessEncoding(DetectString)
    else
      DetectedEncodingName := LConvEncoding.GuessEncoding(DetectString);

    if DetectedEncodingName <> '' then
    begin
      DetectedEncodingName := NormalizeEncoding(DetectedEncodingName);

      // Map UCS-2 to UTF-16.
      if DetectedEncodingName = 'ucs2le' then
        DetectedEncodingName := 'utf16le'
      else if DetectedEncodingName = 'ucs2be' then
        DetectedEncodingName := 'utf16be';

      for Enc := Low(TViewerEncoding) to High(TViewerEncoding) do
      begin
        if NormalizeEncoding(ViewerEncodingsNames[Enc]) = DetectedEncodingName then
        begin
          Result := Enc;
          break;
        end;
      end;
    end;
  end
  else
    Result := veAutoDetect;
end;

procedure TViewerControl.GetSupportedEncodings(List: TStrings);
var
  Enc: TViewerEncoding;
begin
  for Enc := Low(TViewerEncoding) to High(TViewerEncoding) do
    List.Add(ViewerEncodingsNames[Enc]);
end;

function TViewerControl.GetBomLength: Integer;
begin
  Result := 0;

  case FEncoding of
    veUtf8, veUtf8bom:
      if (FFileSize >= 3) and
         (PByte(FMappedFile)[0] = $EF) and
         (PByte(FMappedFile)[1] = $BB) and
         (PByte(FMappedFile)[2] = $BF) then
      begin
        Result := 3;
      end;

    veUcs2be, veUtf16be:
      if (FFileSize >= 2) and
         (PByte(FMappedFile)[0] = $FE) and
         (PByte(FMappedFile)[1] = $FF) then
      begin
        Result := 2;
      end;

    veUcs2le, veUtf16le:
      if (FFileSize >= 2) and
         (PByte(FMappedFile)[0] = $FF) and
         (PByte(FMappedFile)[1] = $FE) then
      begin
        Result := 2;
      end;

    veUtf32be:
      if (FFileSize >= 4) and
         (PByte(FMappedFile)[0] = $00) and
         (PByte(FMappedFile)[1] = $00) and
         (PByte(FMappedFile)[2] = $FE) and
         (PByte(FMappedFile)[3] = $FF) then
      begin
        Result := 4;
      end;

    veUtf32le:
      if (FFileSize >= 4) and
         (PByte(FMappedFile)[0] = $00) and
         (PByte(FMappedFile)[1] = $00) and
         (PByte(FMappedFile)[2] = $FF) and
         (PByte(FMappedFile)[3] = $FE) then
      begin
        Result := 4;
      end;
  end;
end;

function TViewerControl.GetOffsetOfMode(const mode: TViewerControlMode): Integer;
begin
  case mode of
    vcmText, vcmWrap, vcmBook:
      Result:= FBOMLength;
    else
      Result:= 0;
  end;
end;

procedure TViewerControl.UpdateLimits;
begin
  if FEncoding = veAutoDetect then
    FEncoding := DetectEncoding;

  FBOMLength := GetBomLength;

  FLowLimit  := 0;
  FHighLimit := FFileSize - GetOffsetOfMode(FViewerControlMode);
end;

procedure TViewerControl.UpdateSelectionAndCaret;

  procedure Check(var aPosition: PtrInt; Backwards: Boolean);
  var
    CharStart: Pointer;
  begin
    case FEncoding of
      veUtf8, veUtf8bom:
        begin
          if not Backwards then
          begin
            CharStart := SafeUTF8NextCharStart(GetDataAdr + aPosition,
                                               FHighLimit - aPosition);
            if Assigned(CharStart) then
              aPosition := CharStart - GetDataAdr
            else
              aPosition := 0;
          end
          else
          begin
            CharStart := SafeUTF8PrevCharEnd(GetDataAdr + aPosition,
                                             aPosition - FLowLimit);
            if Assigned(CharStart) then
              aPosition := CharStart - GetDataAdr
            else
              aPosition := 0;
          end;
        end;

      veAnsi, veOem,
      veCp1250..veCp874,
      veIso88591,
      veIso88592,
      veKoi8r,
      veKoi8u,
      veKoi8ru:
        ; // any position allowed

      veUcs2be, veUcs2le:
        aPosition := ((aPosition - FLowLimit) and not 1) + FLowLimit;
      veUtf16be, veUtf16le:
        // todo: check if not in the middle of utf-16 character
        aPosition := ((aPosition - FLowLimit) and not 1) + FLowLimit;
      veUtf32be, veUtf32le:
        aPosition := ((aPosition - FLowLimit) and not 3) + FLowLimit;

      veCp932, // Unsupported variable-width encodings
      veCp936, // TODO: Add cp932, cp936, cp949, cp950 encoding support
      veCp949,
      veCp950:
        ;

      else
        raise Exception.Create('Unsupported viewer encoding');
    end;
  end;

begin
  if (FBlockBeg < FLowLimit) or (FBlockBeg >= FHighLimit) or
     (FBlockEnd < FLowLimit) or (FBlockEnd >= FHighLimit) then
  begin
    FBlockBeg := FLowLimit;
    FBlockEnd := FLowLimit;
  end
  else
  begin
    case FViewerControlMode of
      vcmText, vcmWrap, vcmBook:
        begin
          Check(FCaretPos, True);
          Check(FBlockBeg, False);
          Check(FBlockEnd, True);

          if (FBlockBeg < FLowLimit) or (FBlockBeg >= FHighLimit) or
             (FBlockEnd < FLowLimit) or (FBlockEnd >= FHighLimit) or
             (FBlockEnd < FBlockBeg) then
          begin
            FBlockBeg := FLowLimit;
            FBlockEnd := FLowLimit;
          end;
        end;
      // In non-text modes any selection is valid.
    end;
  end;
end;

function TViewerControl.FindUtf8Text(iStartPos: PtrInt; const sSearchText: String;
                                     bCaseSensitive: Boolean; bSearchBackwards: Boolean): PtrInt;
var
  SearchTextLength: Integer;
  sSearchChars: array of String;
  pCurrentAddr, pEndAddr: PtrInt;
  i, charLen: Integer;

  function sPos2(pAdr: PtrInt):Boolean;
  var
    curChr:String;
    i, charLen: Integer;
  begin
    Result := False;
    for i := 0 to SearchTextLength-1 do
    begin
      curChr:=GetNextCharAsUtf8(pAdr,charLen);
      case bCaseSensitive of
       False: if UTF8UpperCase(curChr) <> UTF8UpperCase(sSearchChars[i]) then Exit;
       True : if curChr <> sSearchChars[i] then Exit;
      end;
      if charLen>0 then
        pAdr:=pAdr+charLen
      else
        Inc(pAdr);
    end;
    Result:=True;
  end;

begin
  Result := PtrInt(-1);
  SearchTextLength := UTF8Length(sSearchText);
  if (SearchTextLength <= 0) then
    Exit;

  setLength(sSearchChars,SearchTextLength);
  for i:=1 to SearchTextLength do
    sSearchChars[i-1]:=UTF8Copy(sSearchText,i,1);


  pCurrentAddr := iStartPos;
  pEndAddr := FHighLimit - Length(ConvertFromUTF8(sSearchText));

  if bSearchBackwards and (pCurrentAddr > pEndAddr) then
    // Move to the first possible position for searching backwards.
    pCurrentAddr := pEndAddr;

  if (pEndAddr < 0) or (pCurrentAddr < 0) or (pCurrentAddr > pEndAddr) then
    Exit;

  while True do
  begin
    if (pCurrentAddr > pEndAddr) or (pCurrentAddr < 0) then
      Exit;

    if sPos2(pCurrentAddr) then
    begin
      Result := pCurrentAddr;
      Exit;
    end;

    case bSearchBackwards of
      False:
      begin
        GetNextCharAsUtf8(pCurrentAddr,charLen);
        if charLen>0 then
          pCurrentAddr:=pCurrentAddr+charLen
        else
          Inc(pCurrentAddr);
       end;
      True : Dec(pCurrentAddr);
    end;
  end;
end;

procedure TViewerControl.ResetEncoding;
begin
  FEncoding:= veAutoDetect;
end;

procedure Register;
begin
  RegisterComponents('SeksiCmd', [TViewerControl]);
end;

end.

