# Copyright (c) 2025 Yuki Kimoto
# MIT License

class Mojo::Content::MultiPart extends Mojo::Content {
  version_from Mojo;
  
  use Mojo::Content::Single;
  
  # Fields
  has parts : Mojo::Content[];
  
  # Undocumented Fields
  has last_len : int;
  
  has last_part : int;
  
  has offset : int;
  
  has boundary : string;
  
  has multipart : string;
  
  has multi_state : string;
  
  static method new : Mojo::Content::MultiPart ($options : object[] = undef) {
    
    my $self = new Mojo::Content::MultiPart;
    
    $self->on(read => method : void ($that : Mojo::Content::MultiPart, $chunk : string) {
      $that->_read($chunk);
    });
    
    return $self;
  }
  
  static method new_from_single : Mojo::Content::MultiPart ($single : Mojo::Content::Single, $options : object[] = undef) {
    
    my $self = new Mojo::Content::MultiPart;
    
    copy_fields $self, $single, Mojo::Content;
    
    return $self;
  }
  
  method parse : Mojo::Content ($chunk : string) {
    
    die "TODO";
  }
  
  method body_contains : int ($chunk : string) {
    
    my $parts = $self->{parts};
    
    for my $part (@$parts) {
      
      if ($part->headers_contain($chunk)) {
        return 1;
      }
      elsif ($part->body_contains($chunk)) {
        return 1;
      }
      
    }
    
    return 0;
  }
  
  method body_size : int () {
    
    # Check for existing Content-Length header
    if (my $len = $self->headers->content_length) {
      return Fn->to_int($len);
    }
    
    # Calculate length of whole body
    my $len = my $boundary_len = length($self->build_boundary) + 6;
    
    for my $_ (@{$self->{parts}}) {
      $len += $_->header_size + $_->body_size + $boundary_len;
    }
    
    return $len;
  }

  method build_boundary : string () {
    
    # Check for existing boundary
    my $boundary = $self->boundary;
    if ($boundary) {
      return $boundary;
    }
    # Generate and check boundary
    my $size = 1;
    while (1) {
      
      my $boundary_ascii = new_string_len($size++ * 3);
      for (my $i = 0; $i < length $boundary_ascii; $i++) {
        $boundary_ascii->[$i] = Fn->chr((int)Sys->rand(256))->[0];
      }
      $boundary = Mojo::Util->b64_encode($boundary_ascii);
      my $boundary_ref = [$boundary];
      Re->s($boundary_ref, ["\W", "g"], "X");
      $boundary = $boundary_ref->[0];
      
      unless ($self->body_contains($boundary)) {
        last;
      }
    }
    
    # Add boundary to Content-Type header
    my $headers = $self->headers;
    my $before = "multipart/mixed";
    my $after = "";
    
    if (my $_ = Re->m($headers->content_type // "", "^(.*multipart/[^;]+)(.*)$")) {
      $before = $_->cap1;
      $after = $_->cap2;
    }
    
    $headers->set_content_type("$before; boundary=$boundary$after");
    
    return $boundary;
  }
  
  static method _range : int[] ($start : int, $end : int) {
    
    my $length = $end - $start + 1;
    
    my $nums = new int[$length];
    
    for (my $i = 0; $i < $end + 1; $i++) {
      $nums->[$i] = $start + $i;
    }
    
    return $nums;
  }
  
  method clone : Mojo::Content::MultiPart () {
  
    my $clone = (Mojo::Content::MultiPart)$self->SUPER::clone();
    
    unless ($clone) {
      return undef;
    }
    
    $clone->{parts} = $self->{parts};
    
    return $clone;
  }
  
  method get_body_chunk : string ($offset : int) {
    
    # Body generator
    if ($self->is_dynamic) {
      return $self->generate_body_chunk($offset);
    }
    
    # First boundary
    my $boundary     = $self->{boundary} //= $self->build_boundary;
    my $boundary_len = length($boundary) + 6;
    my $len          = $boundary_len - 2;
    if ($len > $offset) {
      return Fn->substr("--$boundary\x0d\x0a", $offset);
    }
    
    # Skip parts that have already been processed
    my $start = 0;
    if ($self->{offset} && $offset > $self->{offset}) {
      $len = $self->{last_len};
      $start = 1;
    }
    
    # Prepare content part by part
    my $parts = $self->{parts};
    for (my $i = $start; $i < @$parts; $i++) {
      my $part = $parts->[$i];
      
      # Headers
      my $header_len = $part->header_size;
      if (($len + $header_len) > $offset) {
        return $part->get_header_chunk($offset - $len);
      }
      $len += $header_len;
      
      # Content
      my $content_len = $part->body_size;
      if (($len + $content_len) > $offset) {
        return $part->get_body_chunk($offset - $len);
      }
      $len += $content_len;
      
      # Boundary
      if (@$parts - 1 == $i) {
        $boundary .= "--";
        $boundary_len += 2;
      }
      
      if (($len + $boundary_len) > $offset) {
        return Fn->substr("\x0d\x0a--$boundary\x0d\x0a", $offset - $len);
      }
      $len += $boundary_len;
      
      $self->{last_len} = $len;
      $self->{last_part} = $i;
      $self->{offset} = $offset;
    }
  }
  
  method is_multipart : int () {
    return 1;
  }
  
  method _parse_multipart_body : int ($boundary : string) {
    
    # Whole part in buffer
    my $pos = Fn->index($self->{multipart}, "\x0d\x0a--$boundary");
    if ($pos < 0) {
      my $len = length($self->{multipart}) - (length($boundary) + 8);
      
      unless ($len > 0) {
        return 0;
      }
      
      # Store chunk
      my $chunk = Fn->substr($self->{multipart}, 0, $len, "");
      my $parts = $self->{parts};
      
      $parts->[@$parts - 1] = $parts->[@$parts - 1]->parse($chunk);
      
      return 0;
    }
    
    # Store chunk
    my $chunk = Fn->substr($self->{multipart}, 0, $pos, "");
    my $parts = $self->{parts};
    $parts->[@$parts - 1] = $parts->[@$parts - 1]->parse($chunk);
    $self->{multi_state} = "multipart_boundary";
    
    return 1;
  }
  
  method _parse_multipart_boundary : int ($boundary : string) {
    
    # Boundary begins
    if (Fn->index($self->{multipart}, "\x0d\x0a--$boundary\x0d\x0a") == 0) {
      Fn->substr($self->{multipart}, 0, length($boundary) + 6, "");
      
      # New part
      my $part = Mojo::Content::Single->new;
      $part->{relaxed} = 1;
      $self->emit(part => $part);
      $self->{parts} = (Mojo::Content[])Array->merge_object($self->{parts}, [$part]);
      $self->{multi_state} = "multipart_body";
      
      return 1;
    }
    
    # Boundary ends
    my $end = "\x0d\x0a--$boundary--";
    if (Fn->index($self->{multipart}, $end) == 0) {
      Fn->substr($self->{multipart}, 0, length $end, "");
      $self->{multi_state} = "finished";
    }
    
    return 0;
  }
  
  method _parse_multipart_preamble : int ($boundary : string) {
    
    # No boundary yet
    my $pos = -1;
    if (($pos = Fn->index($self->{multipart}, "--$boundary")) < 0) {
      return 0;
    }
    
    # Replace preamble with carriage return and line feed
    Fn->substr($self->{multipart}, 0, $pos, "\x0d\x0a");
    
    # Parse boundary
    $self->{multi_state} = "multipart_boundary";
    
    return 1;
  }

  method _read : void ($chunk : string) {
    
    $self->{multipart} .= $chunk;
    
    my $boundary = $self->boundary;
    while (1) {
      
      if (($self->{multi_state} //= "multipart_preamble") eq "finished") {
        last;
      }
      
      # Preamble
      if ($self->{multi_state} eq "multipart_preamble") {
        unless ($self->_parse_multipart_preamble($boundary)) {
          last;
        }
      }
      
      # Boundary
      elsif ($self->{multi_state} eq "multipart_boundary") {
        unless ($self->_parse_multipart_boundary($boundary)) {
          last;
        }
      }
      
      # Body
      elsif ($self->{multi_state} eq "multipart_body") {
        unless ($self->_parse_multipart_body($boundary)) {
          last;
        }
      }
    }
    
    # Check buffer size
    if (length($self->{multipart} // "") > $self->max_buffer_size) {
      $self->{state} = "finished";
      $self->{limit} = 1;
    }
  }
  
}

__END__

TODO

package Mojo::Content::MultiPart;
use Mojo::Base 'Mojo::Content';

use Mojo::Util qw(b64_encode);

has parts => sub { [] };

sub body_contains {
  my ($self, $chunk) = @_;
  ($_->headers_contain($chunk) or $_->body_contains($chunk)) and return 1 for @{$self->parts};
  return undef;
}

sub body_size {
  my $self = shift;

  # Check for existing Content-Length header
  if (my $len = $self->headers->content_length) { return $len }

  # Calculate length of whole body
  my $len = my $boundary_len = length($self->build_boundary) + 6;
  $len += $_->header_size + $_->body_size + $boundary_len for @{$self->parts};

  return $len;
}

sub build_boundary {
  my $self = shift;

  # Check for existing boundary
  my $boundary;
  return $boundary if defined($boundary = $self->boundary);

  # Generate and check boundary
  my $size = 1;
  do {
    $boundary = b64_encode join('', map chr(rand 256), 1 .. $size++ * 3);
    $boundary =~ s/\W/X/g;
  } while $self->body_contains($boundary);

  # Add boundary to Content-Type header
  my $headers = $self->headers;
  my ($before, $after) = ('multipart/mixed', '');
  ($before, $after) = ($1, $2) if ($headers->content_type // '') =~ m!^(.*multipart/[^;]+)(.*)$!;
  $headers->content_type("$before; boundary=$boundary$after");

  return $boundary;
}

sub clone {
  my $self = shift;
  return undef unless my $clone = $self->SUPER::clone();
  return $clone->parts($self->parts);
}

sub get_body_chunk {
  my ($self, $offset) = @_;

  # Body generator
  return $self->generate_body_chunk($offset) if $self->is_dynamic;

  # First boundary
  my $boundary     = $self->{boundary} //= $self->build_boundary;
  my $boundary_len = length($boundary) + 6;
  my $len          = $boundary_len - 2;
  return substr "--$boundary\x0d\x0a", $offset if $len > $offset;

  # Skip parts that have already been processed
  my $start = 0;
  ($len, $start) = ($self->{last_len}, $self->{last_part} + 1) if $self->{offset} && $offset > $self->{offset};

  # Prepare content part by part
  my $parts = $self->parts;
  for (my $i = $start; $i < @$parts; $i++) {
    my $part = $parts->[$i];

    # Headers
    my $header_len = $part->header_size;
    return $part->get_header_chunk($offset - $len) if ($len + $header_len) > $offset;
    $len += $header_len;

    # Content
    my $content_len = $part->body_size;
    return $part->get_body_chunk($offset - $len) if ($len + $content_len) > $offset;
    $len += $content_len;

    # Boundary
    if ($#$parts == $i) {
      $boundary .= '--';
      $boundary_len += 2;
    }
    return substr "\x0d\x0a--$boundary\x0d\x0a", $offset - $len if ($len + $boundary_len) > $offset;
    $len += $boundary_len;

    @{$self}{qw(last_len last_part offset)} = ($len, $i, $offset);
  }
}

sub is_multipart {1}

sub new {
  my $self = shift->SUPER::new(@_);
  $self->on(read => \&_read);
  return $self;
}

sub _parse_multipart_body {
  my ($self, $boundary) = @_;

  # Whole part in buffer
  my $pos = index $self->{multipart}, "\x0d\x0a--$boundary";
  if ($pos < 0) {
    my $len = length($self->{multipart}) - (length($boundary) + 8);
    return undef unless $len > 0;

    # Store chunk
    my $chunk = substr $self->{multipart}, 0, $len, '';
    $self->parts->[-1] = $self->parts->[-1]->parse($chunk);
    return undef;
  }

  # Store chunk
  my $chunk = substr $self->{multipart}, 0, $pos, '';
  $self->parts->[-1] = $self->parts->[-1]->parse($chunk);
  return !!($self->{multi_state} = 'multipart_boundary');
}

sub _parse_multipart_boundary {
  my ($self, $boundary) = @_;

  # Boundary begins
  if ((index $self->{multipart}, "\x0d\x0a--$boundary\x0d\x0a") == 0) {
    substr $self->{multipart}, 0, length($boundary) + 6, '';

    # New part
    my $part = Mojo::Content::Single->new(relaxed => 1);
    $self->emit(part => $part);
    push @{$self->parts}, $part;
    return !!($self->{multi_state} = 'multipart_body');
  }

  # Boundary ends
  my $end = "\x0d\x0a--$boundary--";
  if ((index $self->{multipart}, $end) == 0) {
    substr $self->{multipart}, 0, length $end, '';
    $self->{multi_state} = 'finished';
  }

  return undef;
}

sub _parse_multipart_preamble {
  my ($self, $boundary) = @_;

  # No boundary yet
  return undef if (my $pos = index $self->{multipart}, "--$boundary") < 0;

  # Replace preamble with carriage return and line feed
  substr $self->{multipart}, 0, $pos, "\x0d\x0a";

  # Parse boundary
  return !!($self->{multi_state} = 'multipart_boundary');
}

sub _read {
  my ($self, $chunk) = @_;

  $self->{multipart} .= $chunk;
  my $boundary = $self->boundary;
  until (($self->{multi_state} //= 'multipart_preamble') eq 'finished') {

    # Preamble
    if ($self->{multi_state} eq 'multipart_preamble') { last unless $self->_parse_multipart_preamble($boundary) }

    # Boundary
    elsif ($self->{multi_state} eq 'multipart_boundary') { last unless $self->_parse_multipart_boundary($boundary) }

    # Body
    elsif ($self->{multi_state} eq 'multipart_body') { last unless $self->_parse_multipart_body($boundary) }
  }

  # Check buffer size
  @$self{qw(state limit)} = ('finished', 1) if length($self->{multipart} // '') > $self->max_buffer_size;
}

1;

=encoding utf8

=head1 NAME

Mojo::Content::MultiPart - HTTP multipart content

=head1 SYNOPSIS

  use Mojo::Content::MultiPart;

  my $multi = Mojo::Content::MultiPart->new;
  $multi->parse('Content-Type: multipart/mixed; boundary=---foobar');
  my $single = $multi->parts->[4];

=head1 DESCRIPTION

L<Mojo::Content::MultiPart> is a container for HTTP multipart content, based on L<RFC
7230|https://tools.ietf.org/html/rfc7230>, L<RFC 7231|https://tools.ietf.org/html/rfc7231> and L<RFC
2388|https://tools.ietf.org/html/rfc2388>.

=head1 EVENTS

L<Mojo::Content::MultiPart> inherits all events from L<Mojo::Content> and can emit the following new ones.

=head2 part

  $multi->on(part => sub ($multi, $single) {...});

Emitted when a new L<Mojo::Content::Single> part starts.

  $multi->on(part => sub ($multi, $single) {
    return unless $single->headers->content_disposition =~ /name="([^"]+)"/;
    say "Field: $1";
  });

=head1 ATTRIBUTES

L<Mojo::Content::MultiPart> inherits all attributes from L<Mojo::Content> and implements the following new ones.

=head2 parts

  my $parts = $multi->parts;
  $multi    = $multi->parts([Mojo::Content::Single->new]);

Content parts embedded in this multipart content, usually L<Mojo::Content::Single> objects.

=head1 METHODS

L<Mojo::Content::MultiPart> inherits all methods from L<Mojo::Content> and implements the following new ones.

=head2 body_contains

  my $bool = $multi->body_contains('foobarbaz');

Check if content parts contain a specific string.

=head2 body_size

  my $size = $multi->body_size;

Content size in bytes.

=head2 build_boundary

  my $boundary = $multi->build_boundary;

Generate a suitable boundary for content and add it to C<Content-Type> header.

=head2 clone

  my $clone = $multi->clone;

Return a new L<Mojo::Content::MultiPart> object cloned from this content if possible, otherwise return C<undef>.

=head2 get_body_chunk

  my $bytes = $multi->get_body_chunk(0);

Get a chunk of content starting from a specific position. Note that it might not be possible to get the same chunk
twice if content was generated dynamically.

=head2 is_multipart

  my $bool = $multi->is_multipart;

True, this is a L<Mojo::Content::MultiPart> object.

=head2 new

  my $multi = Mojo::Content::MultiPart->new;
  my $multi
    = Mojo::Content::MultiPart->new(parts => [Mojo::Content::Single->new]);
  my $multi
    = Mojo::Content::MultiPart->new({parts => [Mojo::Content::Single->new]});

Construct a new L<Mojo::Content::MultiPart> object and subscribe to event L<Mojo::Content/"read"> with default content
parser.

=head1 SEE ALSO

L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.

=cut
