#!/usr/bin/perl
#
# Generate a code point category table and its lookup utilities, using
# Unicode data files as input.
#
# Input: UnicodeData.txt
# Output: unicode_category_table.h
#
# Copyright (c) 2000-2025, PostgreSQL Global Development Group

use strict;
use warnings FATAL => 'all';
use Getopt::Long;

use FindBin;
use lib "$FindBin::RealBin/../../tools/";

my $CATEGORY_UNASSIGNED = 'Cn';

my $output_path = '.';

GetOptions('outdir:s' => \$output_path);

my $output_table_file = "$output_path/unicode_category_table.h";

my $FH;

# create a table of all codepoints < 0x80 and their associated
# categories and properties for fast lookups
my %opt_ascii = ();

# Read entries from UnicodeData.txt into a list of codepoint ranges
# and their general category.
my @category_ranges = ();
my $range_start = undef;
my $range_end = undef;
my $range_category = undef;

# If between a "<..., First>" entry and a "<..., Last>" entry, the gap in
# codepoints represents a range, and $gap_category is equal to the
# category for both (which must match). Otherwise, the gap represents
# unassigned code points.
my $gap_category = undef;

open($FH, '<', "$output_path/UnicodeData.txt")
  or die "Could not open $output_path/UnicodeData.txt: $!.";
while (my $line = <$FH>)
{
	my @elts = split(';', $line);
	my $code = hex($elts[0]);
	my $name = $elts[1];
	my $category = $elts[2];

	die "codepoint out of range" if $code > 0x10FFFF;
	die "unassigned codepoint in UnicodeData.txt"
	  if $category eq $CATEGORY_UNASSIGNED;

	if ($code < 0x80)
	{
		my @properties = ();
		# No ASCII characters have category Titlecase_Letter,
		# but include here for completeness.
		push @properties, "PG_U_PROP_CASED" if ($category eq 'Lt');
		$opt_ascii{$code} = {
			Category => $category,
			Properties => \@properties
		};
	}

	if (!defined($range_start))
	{
		my $code_str = sprintf "0x%06x", $code;
		die
		  if defined($range_end)
		  || defined($range_category)
		  || defined($gap_category);
		die "unexpected first entry <..., Last>" if ($name =~ /Last>/);
		die "expected 0x000000 for first entry, got $code_str"
		  if $code != 0x000000;

		# initialize
		$range_start = $code;
		$range_end = $code;
		$range_category = $category;
		if ($name =~ /<.*, First>$/)
		{
			$gap_category = $category;
		}
		else
		{
			$gap_category = $CATEGORY_UNASSIGNED;
		}
		next;
	}

	# Gap in codepoints detected. If it's a different category than
	# the current range, emit the current range and initialize a new
	# range representing the gap.
	if ($range_end + 1 != $code && $range_category ne $gap_category)
	{
		if ($range_category ne $CATEGORY_UNASSIGNED)
		{
			push(
				@category_ranges,
				{
					start => $range_start,
					end => $range_end,
					category => $range_category
				});
		}
		$range_start = $range_end + 1;
		$range_end = $code - 1;
		$range_category = $gap_category;
	}

	# different category; new range
	if ($range_category ne $category)
	{
		if ($range_category ne $CATEGORY_UNASSIGNED)
		{
			push(
				@category_ranges,
				{
					start => $range_start,
					end => $range_end,
					category => $range_category
				});
		}
		$range_start = $code;
		$range_end = $code;
		$range_category = $category;
	}

	if ($name =~ /<.*, First>$/)
	{
		die
		  "<..., First> entry unexpectedly follows another <..., First> entry"
		  if $gap_category ne $CATEGORY_UNASSIGNED;
		$gap_category = $category;
	}
	elsif ($name =~ /<.*, Last>$/)
	{
		die
		  "<..., First> and <..., Last> entries have mismatching general category"
		  if $gap_category ne $category;
		$gap_category = $CATEGORY_UNASSIGNED;
	}
	else
	{
		die "unexpected entry found between <..., First> and <..., Last>"
		  if $gap_category ne $CATEGORY_UNASSIGNED;
	}

	$range_end = $code;
}
close $FH;

die "<..., First> entry with no corresponding <..., Last> entry"
  if $gap_category ne $CATEGORY_UNASSIGNED;

# emit final range
if ($range_category ne $CATEGORY_UNASSIGNED)
{
	push(
		@category_ranges,
		{
			start => $range_start,
			end => $range_end,
			category => $range_category
		});
}

# See: https://www.unicode.org/reports/tr44/#General_Category_Values
my $categories = {
	Cn => 'PG_U_UNASSIGNED',
	Lu => 'PG_U_UPPERCASE_LETTER',
	Ll => 'PG_U_LOWERCASE_LETTER',
	Lt => 'PG_U_TITLECASE_LETTER',
	Lm => 'PG_U_MODIFIER_LETTER',
	Lo => 'PG_U_OTHER_LETTER',
	Mn => 'PG_U_NONSPACING_MARK',
	Me => 'PG_U_ENCLOSING_MARK',
	Mc => 'PG_U_SPACING_MARK',
	Nd => 'PG_U_DECIMAL_NUMBER',
	Nl => 'PG_U_LETTER_NUMBER',
	No => 'PG_U_OTHER_NUMBER',
	Zs => 'PG_U_SPACE_SEPARATOR',
	Zl => 'PG_U_LINE_SEPARATOR',
	Zp => 'PG_U_PARAGRAPH_SEPARATOR',
	Cc => 'PG_U_CONTROL',
	Cf => 'PG_U_FORMAT',
	Co => 'PG_U_PRIVATE_USE',
	Cs => 'PG_U_SURROGATE',
	Pd => 'PG_U_DASH_PUNCTUATION',
	Ps => 'PG_U_OPEN_PUNCTUATION',
	Pe => 'PG_U_CLOSE_PUNCTUATION',
	Pc => 'PG_U_CONNECTOR_PUNCTUATION',
	Po => 'PG_U_OTHER_PUNCTUATION',
	Sm => 'PG_U_MATH_SYMBOL',
	Sc => 'PG_U_CURRENCY_SYMBOL',
	Sk => 'PG_U_MODIFIER_SYMBOL',
	So => 'PG_U_OTHER_SYMBOL',
	Pi => 'PG_U_INITIAL_PUNCTUATION',
	Pf => 'PG_U_FINAL_PUNCTUATION'
};

# Find White_Space and Hex_Digit characters
my @white_space = ();
my @hex_digits = ();
my @join_control = ();
open($FH, '<', "$output_path/PropList.txt")
  or die "Could not open $output_path/PropList.txt: $!.";
while (my $line = <$FH>)
{
	my $pattern = qr/([0-9A-F\.]+)\s*;\s*(\w+)\s*#.*/s;
	next unless $line =~ $pattern;

	my $code = $line =~ s/$pattern/$1/rg;
	my $property = $line =~ s/$pattern/$2/rg;
	my $start;
	my $end;

	if ($code =~ /\.\./)
	{
		# code range
		my @sp = split /\.\./, $code;
		$start = hex($sp[0]);
		$end = hex($sp[1]);
	}
	else
	{
		# single code point
		$start = hex($code);
		$end = hex($code);
	}

	if ($property eq "White_Space")
	{
		push @white_space, { start => $start, end => $end };
		for (my $i = $start; $i <= $end && $i < 0x80; $i++)
		{
			push @{ $opt_ascii{$i}{Properties} }, "PG_U_PROP_WHITE_SPACE";
		}
	}
	elsif ($property eq "Hex_Digit")
	{
		push @hex_digits, { start => $start, end => $end };
		for (my $i = $start; $i <= $end && $i < 0x80; $i++)
		{
			push @{ $opt_ascii{$i}{Properties} }, "PG_U_PROP_HEX_DIGIT";
		}
	}
	elsif ($property eq "Join_Control")
	{
		push @join_control, { start => $start, end => $end };
		for (my $i = $start; $i <= $end && $i < 0x80; $i++)
		{
			push @{ $opt_ascii{$i}{Properties} }, "PG_U_PROP_JOIN_CONTROL";
		}
	}
}

# Find Alphabetic, Lowercase, and Uppercase characters
my @alphabetic = ();
my @lowercase = ();
my @uppercase = ();
my @case_ignorable = ();
open($FH, '<', "$output_path/DerivedCoreProperties.txt")
  or die "Could not open $output_path/DerivedCoreProperties.txt: $!.";
while (my $line = <$FH>)
{
	my $pattern = qr/^([0-9A-F\.]+)\s*;\s*(\w+)\s*#.*$/s;
	next unless $line =~ $pattern;

	my $code = $line =~ s/$pattern/$1/rg;
	my $property = $line =~ s/$pattern/$2/rg;
	my $start;
	my $end;

	if ($code =~ /\.\./)
	{
		# code range
		my @sp = split /\.\./, $code;
		die "line: {$line} code: {$code} sp[0] {$sp[0]} sp[1] {$sp[1]}"
		  unless $sp[0] =~ /^[0-9A-F]+$/ && $sp[1] =~ /^[0-9A-F]+$/;
		$start = hex($sp[0]);
		$end = hex($sp[1]);
	}
	else
	{
		die "line: {$line} code: {$code}" unless $code =~ /^[0-9A-F]+$/;
		# single code point
		$start = hex($code);
		$end = hex($code);
	}

	if ($property eq "Alphabetic")
	{
		push @alphabetic, { start => $start, end => $end };
		for (my $i = $start; $i <= $end && $i < 0x80; $i++)
		{
			push @{ $opt_ascii{$i}{Properties} }, "PG_U_PROP_ALPHABETIC";
		}
	}
	elsif ($property eq "Lowercase")
	{
		push @lowercase, { start => $start, end => $end };
		for (my $i = $start; $i <= $end && $i < 0x80; $i++)
		{
			push @{ $opt_ascii{$i}{Properties} }, "PG_U_PROP_LOWERCASE";
			push @{ $opt_ascii{$i}{Properties} }, "PG_U_PROP_CASED";
		}
	}
	elsif ($property eq "Uppercase")
	{
		push @uppercase, { start => $start, end => $end };
		for (my $i = $start; $i <= $end && $i < 0x80; $i++)
		{
			push @{ $opt_ascii{$i}{Properties} }, "PG_U_PROP_UPPERCASE";
			push @{ $opt_ascii{$i}{Properties} }, "PG_U_PROP_CASED";
		}
	}
	elsif ($property eq "Case_Ignorable")
	{
		push @case_ignorable, { start => $start, end => $end };
		for (my $i = $start; $i <= $end && $i < 0x80; $i++)
		{
			push @{ $opt_ascii{$i}{Properties} }, "PG_U_PROP_CASE_IGNORABLE";
		}
	}
}

my $num_category_ranges = scalar @category_ranges;
my $num_alphabetic_ranges = scalar @alphabetic;
my $num_lowercase_ranges = scalar @lowercase;
my $num_uppercase_ranges = scalar @uppercase;
my $num_case_ignorable_ranges = scalar @case_ignorable;
my $num_white_space_ranges = scalar @white_space;
my $num_hex_digit_ranges = scalar @hex_digits;
my $num_join_control_ranges = scalar @join_control;

# Start writing out the output file
open my $OT, '>', $output_table_file
  or die "Could not open output file $output_table_file: $!\n";

print $OT <<"EOS";
/*-------------------------------------------------------------------------
 *
 * unicode_category_table.h
 *	  Category table for Unicode character classification.
 *
 * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/include/common/unicode_category_table.h
 *
 *-------------------------------------------------------------------------
 */

#include "common/unicode_category.h"

/*
 * File auto-generated by src/common/unicode/generate-unicode_category_table.pl,
 * do not edit. There is deliberately not an #ifndef PG_UNICODE_CATEGORY_TABLE_H
 * here.
 */
typedef struct
{
	uint32		first;			/* Unicode codepoint */
	uint32		last;			/* Unicode codepoint */
	uint8		category;		/* General Category */
} pg_category_range;

typedef struct
{
	uint32		first;			/* Unicode codepoint */
	uint32		last;			/* Unicode codepoint */
} pg_unicode_range;

typedef struct
{
	uint8		category;
	uint8		properties;
} pg_unicode_properties;

/*
 * The properties currently used, in no particular order. Fits in a uint8, but
 * if more properties are added, a wider integer will be needed.
 */
#define PG_U_PROP_ALPHABETIC		(1 << 0)
#define PG_U_PROP_LOWERCASE			(1 << 1)
#define PG_U_PROP_UPPERCASE			(1 << 2)
#define PG_U_PROP_CASED				(1 << 3)
#define PG_U_PROP_CASE_IGNORABLE	(1 << 4)
#define PG_U_PROP_WHITE_SPACE		(1 << 5)
#define PG_U_PROP_JOIN_CONTROL		(1 << 6)
#define PG_U_PROP_HEX_DIGIT			(1 << 7)

EOS

print $OT <<"EOS";
/* table for fast lookup of ASCII codepoints */
static const pg_unicode_properties unicode_opt_ascii[128] =
{
EOS

for (my $i = 0; $i < 128; $i++)
{
	my $category_str = $categories->{ $opt_ascii{$i}->{Category} };
	my $props_str = (join ' | ', @{ $opt_ascii{$i}{Properties} }) || "0";
	printf $OT
	  "\t{\n\t\t/* 0x%06x */\n\t\t.category = %s,\n\t\t.properties = %s\n\t},\n",
	  $i, $category_str, $props_str;
}

print $OT "};\n\n";

print $OT <<"EOS";
/* table of Unicode codepoint ranges and their categories */
static const pg_category_range unicode_categories[$num_category_ranges] =
{
EOS

foreach my $range (@category_ranges)
{
	my $category = $categories->{ $range->{category} };
	die "category missing: $range->{category}" unless $category;
	printf $OT "\t{0x%06x, 0x%06x, %s},\n", $range->{start}, $range->{end},
	  $category;
}

print $OT "};\n\n";

print $OT <<"EOS";
/* table of Unicode codepoint ranges of Alphabetic characters */
static const pg_unicode_range unicode_alphabetic[$num_alphabetic_ranges] =
{
EOS

foreach my $range (@alphabetic)
{
	printf $OT "\t{0x%06x, 0x%06x},\n", $range->{start}, $range->{end};
}

print $OT "};\n\n";

print $OT <<"EOS";
/* table of Unicode codepoint ranges of Lowercase characters */
static const pg_unicode_range unicode_lowercase[$num_lowercase_ranges] =
{
EOS

foreach my $range (@lowercase)
{
	printf $OT "\t{0x%06x, 0x%06x},\n", $range->{start}, $range->{end};
}

print $OT "};\n\n";

print $OT <<"EOS";
/* table of Unicode codepoint ranges of Uppercase characters */
static const pg_unicode_range unicode_uppercase[$num_uppercase_ranges] =
{
EOS

foreach my $range (@uppercase)
{
	printf $OT "\t{0x%06x, 0x%06x},\n", $range->{start}, $range->{end};
}

print $OT "};\n\n";

print $OT <<"EOS";
/* table of Unicode codepoint ranges of Case_Ignorable characters */
static const pg_unicode_range unicode_case_ignorable[$num_case_ignorable_ranges] =
{
EOS

foreach my $range (@case_ignorable)
{
	printf $OT "\t{0x%06x, 0x%06x},\n", $range->{start}, $range->{end};
}

print $OT "};\n\n";

print $OT <<"EOS";
/* table of Unicode codepoint ranges of White_Space characters */
static const pg_unicode_range unicode_white_space[$num_white_space_ranges] =
{
EOS

foreach my $range (@white_space)
{
	printf $OT "\t{0x%06x, 0x%06x},\n", $range->{start}, $range->{end};
}

print $OT "};\n\n";

print $OT <<"EOS";
/* table of Unicode codepoint ranges of Hex_Digit characters */
static const pg_unicode_range unicode_hex_digit[$num_hex_digit_ranges] =
{
EOS

foreach my $range (@hex_digits)
{
	printf $OT "\t{0x%06x, 0x%06x},\n", $range->{start}, $range->{end};
}

print $OT "};\n\n";

print $OT <<"EOS";
/* table of Unicode codepoint ranges of Join_Control characters */
static const pg_unicode_range unicode_join_control[$num_join_control_ranges] =
{
EOS

foreach my $range (@join_control)
{
	printf $OT "\t{0x%06x, 0x%06x},\n", $range->{start}, $range->{end};
}

print $OT "};\n";
