--  Copyright © 2026 Lingenic LLC. All rights reserved.
--  Licensed under the Lingenic Source-Available License v2.3.
--  Production use requires a separate license from Licensor.
--  See LICENSE.md and COPYRIGHT in the project root.
--

-------------------------------------------------------------------------------
--  Lingenic-Text
--  Formally Verified Unicode Text Processing Library
--
--  UCD file format ghost specification: UAX #44 §4.2 rules as expression
--  functions.
--
--  Encodes the standard property file format used by Scripts.txt,
--  GraphemeBreakProperty.txt, WordBreakProperty.txt, etc.:
--
--    codepoint_or_range ; property_value # comment
--
--  Where codepoint_or_range is XXXX or XXXX..YYYY (4-6 uppercase hex digits,
--  no prefix).  Lines starting with '#' are comments.  Blank lines are
--  skipped.  Whitespace around fields is trimmed.
--
--  Every function here is Ghost — erased at compile time, zero runtime cost.
--  The solver unfolds expression function bodies directly into proof contexts.
--
--  The parser's postconditions reference these ghost functions to establish
--  that the parsed table matches what the UCD file specifies.
-------------------------------------------------------------------------------

package Lingenic_Text.UCD_Format_Spec
   with SPARK_Mode, Ghost, Pure
is

   ---------------------------------------------------------------------------
   --  Character classification and byte constants are inherited from the
   --  root package Lingenic_Text:
   --    Is_Hex_Digit, Hex_Value, Is_Line_End, Is_Field_Space
   --    LF_Byte, CR_Byte, Space_Byte, Tab_Byte, Hash_Byte, Dot_Byte,
   --    Semicolon_Byte, Zero_Byte, Nine_Byte, Upper_A_Byte, Upper_F_Byte
   ---------------------------------------------------------------------------
   --  Layer 2: Scanning helpers
   --
   --  All require Pos in Source'Range and Source'First = 1.
   --  Source'First = 1 ensures Source'Last >= 1 when Source is non-empty,
   --  which keeps all arithmetic in Positive and avoids overflow.
   --
   --  Subprogram_Variant uses (Source'Last - Pos) which is always >= 0
   --  because Pos in Source'Range implies Pos <= Source'Last.
   ---------------------------------------------------------------------------

   --  Count of consecutive hex digits starting at Pos.
   --  Returns 0 if Source(Pos) is not a hex digit.
   function Hex_Digit_Count
     (Source : Byte_Array;
      Pos    : Positive) return Natural
   is (if not Is_Hex_Digit (Source (Pos)) then 0
       elsif Pos = Source'Last then 1
       else 1 + Hex_Digit_Count (Source, Pos + 1))
   with Pre  => Source'First = 1
                and then Source'Last < Positive'Last
                and then Pos in Source'Range,
        Subprogram_Variant => (Decreases => Source'Last - Pos),
        Post => Hex_Digit_Count'Result <= Source'Last - Pos + 1;

   --  Parse a hex number from Len consecutive hex digits at Pos.
   --  Non-recursive: expressed as direct arithmetic per length.
   --  Len is restricted to 1..6 (UCD hex is 4-6, this covers all).
   --
   --  Maximum values by length:
   --    1 digit:  F         = 15
   --    2 digits: FF        = 255
   --    3 digits: FFF       = 4095
   --    4 digits: FFFF      = 65535
   --    5 digits: FFFFF     = 1048575
   --    6 digits: 10FFFF    = 1114111  (max codepoint)
   function Parse_Hex_1
     (Source : Byte_Array;
      Pos    : Positive) return Natural
   is (Hex_Value (Source (Pos)))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Pos in Source'Range
               and then Is_Hex_Digit (Source (Pos));

   function Parse_Hex_2
     (Source : Byte_Array;
      Pos    : Positive) return Natural
   is (Hex_Value (Source (Pos)) * 16
       + Hex_Value (Source (Pos + 1)))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Pos in Source'Range
               and then Pos + 1 <= Source'Last
               and then Is_Hex_Digit (Source (Pos))
               and then Is_Hex_Digit (Source (Pos + 1));

   function Parse_Hex_3
     (Source : Byte_Array;
      Pos    : Positive) return Natural
   is (Hex_Value (Source (Pos)) * 256
       + Hex_Value (Source (Pos + 1)) * 16
       + Hex_Value (Source (Pos + 2)))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Pos in Source'Range
               and then Source'Last - Pos >= 2
               and then Is_Hex_Digit (Source (Pos))
               and then Is_Hex_Digit (Source (Pos + 1))
               and then Is_Hex_Digit (Source (Pos + 2));

   function Parse_Hex_4
     (Source : Byte_Array;
      Pos    : Positive) return Natural
   is (Hex_Value (Source (Pos)) * 4096
       + Hex_Value (Source (Pos + 1)) * 256
       + Hex_Value (Source (Pos + 2)) * 16
       + Hex_Value (Source (Pos + 3)))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Pos in Source'Range
               and then Source'Last - Pos >= 3
               and then Is_Hex_Digit (Source (Pos))
               and then Is_Hex_Digit (Source (Pos + 1))
               and then Is_Hex_Digit (Source (Pos + 2))
               and then Is_Hex_Digit (Source (Pos + 3));

   function Parse_Hex_5
     (Source : Byte_Array;
      Pos    : Positive) return Natural
   is (Hex_Value (Source (Pos)) * 65536
       + Hex_Value (Source (Pos + 1)) * 4096
       + Hex_Value (Source (Pos + 2)) * 256
       + Hex_Value (Source (Pos + 3)) * 16
       + Hex_Value (Source (Pos + 4)))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Pos in Source'Range
               and then Source'Last - Pos >= 4
               and then Is_Hex_Digit (Source (Pos))
               and then Is_Hex_Digit (Source (Pos + 1))
               and then Is_Hex_Digit (Source (Pos + 2))
               and then Is_Hex_Digit (Source (Pos + 3))
               and then Is_Hex_Digit (Source (Pos + 4));

   function Parse_Hex_6
     (Source : Byte_Array;
      Pos    : Positive) return Natural
   is (Hex_Value (Source (Pos)) * 1048576
       + Hex_Value (Source (Pos + 1)) * 65536
       + Hex_Value (Source (Pos + 2)) * 4096
       + Hex_Value (Source (Pos + 3)) * 256
       + Hex_Value (Source (Pos + 4)) * 16
       + Hex_Value (Source (Pos + 5)))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Pos in Source'Range
               and then Source'Last - Pos >= 5
               and then Is_Hex_Digit (Source (Pos))
               and then Is_Hex_Digit (Source (Pos + 1))
               and then Is_Hex_Digit (Source (Pos + 2))
               and then Is_Hex_Digit (Source (Pos + 3))
               and then Is_Hex_Digit (Source (Pos + 4))
               and then Is_Hex_Digit (Source (Pos + 5));

   --  Dispatch to the right Parse_Hex_N by length.
   function Parse_Hex
     (Source : Byte_Array;
      Pos    : Positive;
      Len    : Positive) return Natural
   is (case Len is
         when 1 => Parse_Hex_1 (Source, Pos),
         when 2 => Parse_Hex_2 (Source, Pos),
         when 3 => Parse_Hex_3 (Source, Pos),
         when 4 => Parse_Hex_4 (Source, Pos),
         when 5 => Parse_Hex_5 (Source, Pos),
         when 6 => Parse_Hex_6 (Source, Pos),
         when others => 0)
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Pos in Source'Range
               and then Len in 4 .. 6
               and then Source'Last - Pos >= Len - 1
               and then (for all I in Pos .. Pos + Len - 1 =>
                           Is_Hex_Digit (Source (I)));

   ---------------------------------------------------------------------------
   --  Layer 3: Line structure scanning
   --
   --  These scan forward within a line or across lines.
   ---------------------------------------------------------------------------

   --  Find the start of the next line after Pos (position after LF).
   --  Returns Source'Last + 1 if no LF found at or after Pos.
   function Next_Line_Start
     (Source : Byte_Array;
      Pos    : Positive) return Positive
   is (if Source (Pos) = LF_Byte then
         (if Pos < Source'Last then Pos + 1 else Source'Last + 1)
       elsif Pos = Source'Last then Source'Last + 1
       else Next_Line_Start (Source, Pos + 1))
   with Pre  => Source'First = 1
                and then Source'Last < Positive'Last
                and then Pos in Source'Range,
        Subprogram_Variant => (Decreases => Source'Last - Pos),
        Post => Next_Line_Start'Result > Pos
                and then Next_Line_Start'Result <= Source'Last + 1;

   --  Find the first non-whitespace byte at or after Pos on the current line.
   --  Returns 0 if only whitespace/line-end remains.
   function Skip_Spaces
     (Source : Byte_Array;
      Pos    : Positive) return Natural
   is (if Is_Line_End (Source (Pos)) then 0
       elsif not Is_Field_Space (Source (Pos)) then Pos
       elsif Pos = Source'Last then 0
       else Skip_Spaces (Source, Pos + 1))
   with Pre  => Source'First = 1
                and then Source'Last < Positive'Last
                and then Pos in Source'Range,
        Subprogram_Variant => (Decreases => Source'Last - Pos),
        Post => (if Skip_Spaces'Result > 0 then
                   Skip_Spaces'Result in Pos .. Source'Last);

   --  Find the semicolon on the current line at or after Pos.
   --  Returns 0 if no semicolon found before line end.
   function Find_Semicolon
     (Source : Byte_Array;
      Pos    : Positive) return Natural
   is (if Is_Line_End (Source (Pos)) then 0
       elsif Source (Pos) = Semicolon_Byte then Pos
       elsif Pos = Source'Last then 0
       else Find_Semicolon (Source, Pos + 1))
   with Pre  => Source'First = 1
                and then Source'Last < Positive'Last
                and then Pos in Source'Range,
        Subprogram_Variant => (Decreases => Source'Last - Pos),
        Post => (if Find_Semicolon'Result > 0 then
                   Find_Semicolon'Result in Pos .. Source'Last);

   --  Find the end of the value field: position of '#', LF, CR, or past end.
   function Find_Value_End
     (Source : Byte_Array;
      Pos    : Positive) return Positive
   is (if Source (Pos) = Hash_Byte then Pos
       elsif Is_Line_End (Source (Pos)) then Pos
       elsif Pos = Source'Last then Source'Last + 1
       else Find_Value_End (Source, Pos + 1))
   with Pre  => Source'First = 1
                and then Source'Last < Positive'Last
                and then Pos in Source'Range,
        Subprogram_Variant => (Decreases => Source'Last - Pos),
        Post => Find_Value_End'Result in Pos .. Source'Last + 1;

   ---------------------------------------------------------------------------
   --  Layer 4: Data line predicates and field extraction
   --
   --  A well-formed UCD data line:
   --    - First non-space character is a hex digit
   --    - 4-6 hex digits for the first codepoint
   --    - Contains a semicolon before line end
   --    - Has enough room for the hex field before the semicolon
   ---------------------------------------------------------------------------

   --  Is this line a data line?
   --  Requires: starts with hex (4-6 digits), has a semicolon.
   function Is_Data_Line
     (Source     : Byte_Array;
      Line_Start : Positive) return Boolean
   is (Skip_Spaces (Source, Line_Start) > 0
       and then Is_Hex_Digit (Source (Skip_Spaces (Source, Line_Start)))
       and then Hex_Digit_Count (Source,
                  Skip_Spaces (Source, Line_Start)) in 4 .. 6
       and then Find_Semicolon (Source, Line_Start) > 0)
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Line_Start in Source'Range;

   --  Does the data line contain a range (XXXX..YYYY)?
   --  After the first hex field there must be ".." followed by 4-6 hex digits.
   function Is_Range_Line
     (Source     : Byte_Array;
      Line_Start : Positive) return Boolean
   is (Is_Data_Line (Source, Line_Start)
       and then (declare
                   F0 : constant Positive :=
                     Skip_Spaces (Source, Line_Start);
                   HC : constant Natural :=
                     Hex_Digit_Count (Source, F0);
                 begin
                   --  HC <= Source'Last - F0 + 1 (from Hex_Digit_Count Post),
                   --  so F0 + HC <= Source'Last + 1 <= Positive'Last (safe).
                   --  We need at least ".." + 4 hex digits after the first hex
                   --  field, so Source'Last - (F0 + HC) >= 5 minimum.
                   Source'Last - F0 >= HC + 1
                   and then Source (F0 + HC) = Dot_Byte
                   and then Source (F0 + HC + 1) = Dot_Byte
                   and then Source'Last - F0 >= HC + 2
                   and then Hex_Digit_Count (Source, F0 + HC + 2) in 4 .. 6))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Line_Start in Source'Range;

   --  Extract the first (or only) codepoint from a data line.
   function Line_First_CP
     (Source     : Byte_Array;
      Line_Start : Positive) return Natural
   is (declare
         F0  : constant Positive := Skip_Spaces (Source, Line_Start);
         HC  : constant Natural  := Hex_Digit_Count (Source, F0);
       begin
         Parse_Hex (Source, F0, HC))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Line_Start in Source'Range
               and then Is_Data_Line (Source, Line_Start);

   --  Extract the second codepoint from a range line (after "..").
   function Line_Last_CP
     (Source     : Byte_Array;
      Line_Start : Positive) return Natural
   is (declare
         F0   : constant Positive := Skip_Spaces (Source, Line_Start);
         HC1  : constant Natural  := Hex_Digit_Count (Source, F0);
         P2   : constant Positive := F0 + HC1 + 2;
         HC2  : constant Natural  := Hex_Digit_Count (Source, P2);
       begin
         Parse_Hex (Source, P2, HC2))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Line_Start in Source'Range
               and then Is_Range_Line (Source, Line_Start);

   ---------------------------------------------------------------------------
   --  Layer 5: Value field location
   --
   --  The property value is field 1: after ';', before '#' or line end,
   --  leading/trailing spaces trimmed.
   ---------------------------------------------------------------------------

   --  Start of the trimmed value string (first non-space after ';').
   --  Returns 0 if the value field is empty/whitespace-only.
   function Value_Start
     (Source     : Byte_Array;
      Line_Start : Positive) return Natural
   is (declare
         Semi : constant Natural := Find_Semicolon (Source, Line_Start);
       begin
         (if Semi >= Source'Last then 0
          else Skip_Spaces (Source, Semi + 1)))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Line_Start in Source'Range
               and then Is_Data_Line (Source, Line_Start);

   --  End of the value field (position of '#', LF, CR, or past end).
   function Value_End
     (Source     : Byte_Array;
      Line_Start : Positive) return Positive
   is (declare
         Semi : constant Natural := Find_Semicolon (Source, Line_Start);
       begin
         (if Semi >= Source'Last then Source'Last + 1
          else Find_Value_End (Source, Semi + 1)))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Line_Start in Source'Range
               and then Is_Data_Line (Source, Line_Start);

   --  Trim trailing field spaces from the byte range [First .. Last - 1].
   --  Returns the new end position: the first position after the last
   --  non-space byte.  If the entire range is spaces, returns First
   --  (empty field).
   --
   --  This models the implementation's Trim_Trailing helper, which the parser
   --  applies to the value field before matching against property names.
   function Trim_Spaces_End
     (Source : Byte_Array;
      First  : Positive;
      Last   : Positive) return Positive
   is (if Last <= First then First
       elsif not Is_Field_Space (Source (Last - 1)) then Last
       else Trim_Spaces_End (Source, First, Last - 1))
   with Pre  => Source'First = 1
                and then Source'Last < Positive'Last
                and then First in Source'Range
                and then Last in First .. Source'Last + 1,
        Subprogram_Variant => (Decreases => Last - First),
        Post => Trim_Spaces_End'Result in First .. Last;

   ---------------------------------------------------------------------------
   --  Layer 6: File-level assignment
   --
   --  "Does the data line at Line_Start assign a value to codepoint CP?"
   ---------------------------------------------------------------------------

   function Line_Covers_CP
     (Source     : Byte_Array;
      Line_Start : Positive;
      CP         : Codepoint) return Boolean
   is (Is_Data_Line (Source, Line_Start)
       and then
         (if Is_Range_Line (Source, Line_Start) then
            CP >= Line_First_CP (Source, Line_Start)
            and then CP <= Line_Last_CP (Source, Line_Start)
          else
            CP = Line_First_CP (Source, Line_Start)))
   with Pre => Source'First = 1
               and then Source'Last < Positive'Last
               and then Line_Start in Source'Range;

   ---------------------------------------------------------------------------
   --  Layer 7: Byte range comparison
   --
   --  Used by the parser's ghost matching functions to compare a value field
   --  against known property value names.
   ---------------------------------------------------------------------------

   --  Are the byte ranges [A_First .. A_First + Len - 1] and
   --  [B_First .. B_First + Len - 1] equal byte-by-byte?
   --  Len = 0 means trivially equal.
   function Bytes_Equal
     (Source  : Byte_Array;
      A_First : Positive;
      B_First : Positive;
      Len     : Natural) return Boolean
   is (if Len = 0 then True
       else Source (A_First) = Source (B_First)
            and then (if Len = 1 then True
                      else Bytes_Equal (Source, A_First + 1, B_First + 1,
                                        Len - 1)))
   with Pre  => Source'First = 1
                and then Source'Last < Positive'Last
                and then (if Len > 0 then
                            A_First in Source'Range
                            and then B_First in Source'Range
                            and then Source'Last - A_First >= Len - 1
                            and then Source'Last - B_First >= Len - 1),
        Subprogram_Variant => (Decreases => Len);

end Lingenic_Text.UCD_Format_Spec;