package Eshu;

use 5.008003;
use strict;
use warnings;

our $VERSION = '0.01';

require XSLoader;
XSLoader::load('Eshu', $VERSION);

use File::Find ();
use File::Spec ();

my $MAX_FILE_SIZE = 1_048_576;  # 1MB

sub indent_file {
	my ($class, $path, %opts) = @_;

	my $result = {
		file   => $path,
		status => 'unchanged',
	};

	# Check file exists and is readable
	unless (-f $path && -r _) {
		$result->{status} = 'error';
		$result->{error}  = 'not a readable file';
		return $result;
	}

	# Check file size
	my $size = -s $path;
	if ($size > $MAX_FILE_SIZE) {
		$result->{status} = 'skipped';
		$result->{reason} = 'file too large';
		return $result;
	}

	# Read file
	open my $fh, '<', $path or do {
		$result->{status} = 'error';
		$result->{error}  = "cannot open: $!";
		return $result;
	};
	my $src = do { local $/; <$fh> };
	close $fh;

	# Binary detection: NUL in first 8KB
	if (length($src) > 0) {
		my $sample = substr($src, 0, 8192);
		if ($sample =~ /\0/) {
			$result->{status} = 'skipped';
			$result->{reason} = 'binary file';
			return $result;
		}
	}

	# Detect language
	my $lang = $opts{lang} || $class->detect_lang($path);
	unless (defined $lang) {
		$result->{status} = 'skipped';
		$result->{reason} = 'unrecognised extension';
		return $result;
	}

	# Build indent options
	my %indent_opts = (lang => $lang);
	for my $k (qw(indent_char indent_width indent_pp range_start range_end)) {
		$indent_opts{$k} = $opts{$k} if exists $opts{$k};
	}

	# Indent
	my $fixed = $class->indent_string($src, %indent_opts);

	if ($fixed eq $src) {
		$result->{status} = 'unchanged';
	} else {
		$result->{status} = $opts{fix} ? 'changed' : 'needs_fixing';
		if ($opts{fix}) {
			open my $out, '>', $path or do {
				$result->{status} = 'error';
				$result->{error}  = "cannot write: $!";
				return $result;
			};
			print $out $fixed;
			close $out;
		}
		$result->{diff} = _simple_diff($path, $src, $fixed) if $opts{diff};
	}
	$result->{lang} = $lang;
	return $result;
}

sub indent_dir {
	my ($class, $path, %opts) = @_;

	my $recursive = exists $opts{recursive} ? $opts{recursive} : 1;
	my $exclude   = $opts{exclude};
	my $include   = $opts{include};

	my @files;

	if (-f $path) {
		push @files, $path;
	} elsif (-d $path) {
		if ($recursive) {
			File::Find::find({
				wanted => sub {
					return unless -f $_;
					return if -l $File::Find::dir && $File::Find::dir ne $path;
					push @files, $File::Find::name;
				},
				follow_skip => 2,
				no_chdir    => 1,
			}, $path);
		} else {
			opendir my $dh, $path or die "Eshu: cannot opendir '$path': $!\n";
			while (my $entry = readdir $dh) {
				next if $entry =~ /^\./;
				my $full = File::Spec->catfile($path, $entry);
				push @files, $full if -f $full;
			}
			closedir $dh;
		}
	} else {
		die "Eshu: '$path' is not a file or directory\n";
	}

	@files = sort @files;

	my $report = {
		files_checked => 0,
		files_changed => 0,
		files_skipped => 0,
		files_errored => 0,
		changes       => [],
	};

	for my $file (@files) {
		# Apply exclude filter
		if (defined $exclude) {
			my @pats = ref $exclude eq 'ARRAY' ? @$exclude : ($exclude);
			my $skip = 0;
			for my $pat (@pats) {
				if ($file =~ $pat) { $skip = 1; last; }
			}
			if ($skip) {
				$report->{files_skipped}++;
				push @{$report->{changes}}, { file => $file, status => 'skipped', reason => 'excluded' };
				next;
			}
		}

		# Apply include filter
		if (defined $include) {
			my @pats = ref $include eq 'ARRAY' ? @$include : ($include);
			my $match = 0;
			for my $pat (@pats) {
				if ($file =~ $pat) { $match = 1; last; }
			}
			unless ($match) {
				$report->{files_skipped}++;
				push @{$report->{changes}}, { file => $file, status => 'skipped', reason => 'not included' };
				next;
			}
		}

		my $result = $class->indent_file($file, %opts);
		push @{$report->{changes}}, $result;

		if ($result->{status} eq 'changed' || $result->{status} eq 'needs_fixing') {
			$report->{files_changed}++;
			$report->{files_checked}++;
		} elsif ($result->{status} eq 'unchanged') {
			$report->{files_checked}++;
		} elsif ($result->{status} eq 'skipped') {
			$report->{files_skipped}++;
		} elsif ($result->{status} eq 'error') {
			$report->{files_errored}++;
		}
	}

	return $report;
}

sub _simple_diff {
	my ($label, $old, $new) = @_;
	my @old_lines = split /^/m, $old;
	my @new_lines = split /^/m, $new;
	my $out = "--- a/$label\n+++ b/$label\n";
	my $max = @old_lines > @new_lines ? scalar @old_lines : scalar @new_lines;
	my $hunk_start = -1;
	my (@hunk_old, @hunk_new);

	for (my $i = 0; $i <= $max; $i++) {
		my $ol = $i < @old_lines ? $old_lines[$i] : undef;
		my $nl = $i < @new_lines ? $new_lines[$i] : undef;
		my $same = defined $ol && defined $nl && $ol eq $nl;

		if (!$same && $hunk_start < 0) {
			$hunk_start = $i;
			@hunk_old = ();
			@hunk_new = ();
		}
		if ($hunk_start >= 0 && !$same) {
			push @hunk_old, $ol if defined $ol;
			push @hunk_new, $nl if defined $nl;
		}
		if ($same || $i == $max) {
			if ($hunk_start >= 0) {
				$out .= sprintf "@@ -%d,%d +%d,%d @@\n",
					$hunk_start + 1, scalar @hunk_old,
					$hunk_start + 1, scalar @hunk_new;
				for my $l (@hunk_old) {
					$l = "$l\n" unless $l =~ /\n$/;
					$out .= "-$l";
				}
				for my $l (@hunk_new) {
					$l = "$l\n" unless $l =~ /\n$/;
					$out .= "+$l";
				}
				$hunk_start = -1;
			}
		}
	}
	return $out;
}

1;

__END__

=head1 NAME

Eshu - Fast indentation fixer for C, Perl and XS source files

=head1 SYNOPSIS

    use Eshu;

    # Fix indentation for a specific language
    my $fixed_c    = Eshu->indent_c($source);
    my $fixed_pl   = Eshu->indent_pl($source);
    my $fixed_xs   = Eshu->indent_xs($source);

    # Auto-dispatch by language name
    my $fixed = Eshu->indent_string($source, lang => 'perl');

    # Use spaces instead of tabs
    my $fixed = Eshu->indent_c($source,
        indent_char  => ' ',
        indent_width => 4,
    );

    # Detect language from filename
    my $lang = Eshu->detect_lang('lib/Foo.pm');  # 'perl'

=head1 DESCRIPTION

Eshu is an XS-powered indentation fixer that rewrites leading whitespace in
C, Perl and XS source files. It tracks brace/bracket/paren nesting depth and
re-emits each line with correct indentation while leaving the content of each
line untouched.

Eshu understands language-specific constructs that affect indentation:

=over 4

=item B<C> — strings, comments, preprocessor directives, block nesting

=item B<Perl> — heredocs, regex, C<qw()>/C<qq()>/C<q()>, C<s///>/C<tr///>/C<y///>, pod sections, comments

=item B<XS> — dual-mode scanning (C section above C<MODULE =>, XS section below), XSUB boundaries, label detection (C<CODE:>, C<OUTPUT:>, C<BOOT:>, etc.), C nesting within code body sections

=back

The engine is written in C for speed and operates as a single-pass
line-by-line scanner.

=head1 METHODS

=head2 indent_c

    my $out = Eshu->indent_c($source, %opts);

Fix indentation of C source code. Tracks C<{}>, C<()>, C<[]> nesting,
handles C strings, character literals, line and block comments, and
preprocessor directives.

=head2 indent_pl

    my $out = Eshu->indent_pl($source, %opts);

Fix indentation of Perl source code. In addition to brace nesting, handles
heredocs (C<< <<EOF >>, C<< <<~EOF >>, C<< <<'EOF' >>, C<< <<"EOF" >>),
regex literals, C<qw()>/C<qq()>/C<q()> constructs with paired and non-paired
delimiters, multi-section operators (C<s///>, C<tr///>, C<y///>), pod
sections (C<=head1> through C<=cut>), and line comments.

=head2 indent_xs

    my $out = Eshu->indent_xs($source, %opts);

Fix indentation of XS source files. Operates in dual mode: lines above the
first C<MODULE = ...> line are processed as C; lines below are processed as
XS with XSUB boundary detection, label recognition, and C nesting tracking
within code body sections. C<BOOT:> sections use a shallower indentation
depth than other labels.

=head2 indent_string

    my $out = Eshu->indent_string($source, lang => $lang, %opts);

Dispatch to the appropriate engine based on the C<lang> parameter:

=over 4

=item C<c> (default) — calls L</indent_c>

=item C<perl> or C<pl> — calls L</indent_pl>

=item C<xs> — calls L</indent_xs>

=back

=head2 detect_lang

    my $lang = Eshu->detect_lang($filename);

Return a language string based on the file extension, suitable for passing
to L</indent_string>. Returns C<undef> for unrecognised extensions.

    .c, .h       → 'c'
    .xs          → 'xs'
    .pl, .pm, .t → 'perl'

=head1 OPTIONS

All indentation methods accept the following options as key-value pairs:

=over 4

=item B<indent_char>

Character to use for indentation. Either C<"\t"> (tab, the default) or
C<" "> (space).

=item B<indent_width>

Number of characters per indentation level. Defaults to C<1> for tabs.
Typically set to C<2> or C<4> when using spaces.

=item B<indent_pp>

Boolean. When true, indent C preprocessor directives according to their
C<#if>/C<#endif> nesting depth. Defaults to C<0> (preprocessor directives
stay at column 0). Only meaningful for C and XS engines.

=back

=head1 CLI

Eshu ships with a command-line tool:

    # Fix a file in-place
    eshu --fix lib/Foo.pm

    # Preview changes as a diff
    eshu --diff lib/Foo.pm

    # CI check — exit 1 if file would change
    eshu --check lib/Foo.pm

    # Read stdin, write stdout
    cat messy.c | eshu --lang c

    # Use 4-space indentation
    eshu --spaces 4 --fix src/bar.c

Run C<eshu --help> for the full list of options.

=head1 AUTHOR

LNATION E<lt>email@lnation.orgE<gt>

=head1 LICENSE

This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut
