package Travel::Status::DE::HAFAS;

use strict;
use warnings;
use 5.014;
use utf8;

no if $] >= 5.018, warnings => 'experimental::smartmatch';

use Carp qw(confess);
use DateTime;
use DateTime::Format::Strptime;
use Digest::MD5 qw(md5_hex);
use Encode      qw(decode encode);
use JSON;
use List::Util qw(any);
use LWP::UserAgent;
use POSIX qw(strftime);
use Travel::Status::DE::HAFAS::Message;
use Travel::Status::DE::HAFAS::Result;
use Travel::Status::DE::HAFAS::StopFinder;
use XML::LibXML;

our $VERSION = '3.01';

my %hafas_instance = (
	DB => {
		stopfinder  => 'https://reiseauskunft.bahn.de/bin/ajax-getstop.exe',
		mgate       => 'https://reiseauskunft.bahn.de/bin/mgate.exe',
		name        => 'Deutsche Bahn',
		productbits => [qw[ice ic_ec d regio s bus ferry u tram ondemand]],
		salt        => 'bdI8UVj4' . '0K5fvxwf',
		request     => {
			client => {
				id   => 'DB',
				v    => '20100000',
				type => 'IPH',
				name => 'DB Navigator',
			},
			ext  => 'DB.R21.12.a',
			ver  => '1.15',
			auth => {
				type => 'AID',
				aid  => 'n91dB8Z77' . 'MLdoR0K'
			},
		},
	},
	NAHSH => {
		mgate       => 'https://nah.sh.hafas.de/bin/mgate.exe',
		stopfinder  => 'https://nah.sh.hafas.de/bin/ajax-getstop.exe',
		name        => 'Nahverkehrsverbund Schleswig-Holstein',
		productbits => [qw[ice ice ice regio s bus ferry u tram ondemand]],
		request     => {
			client => {
				id   => 'NAHSH',
				v    => '3000700',
				type => 'IPH',
				name => 'NAHSHPROD',
			},
			ver  => '1.16',
			auth => {
				type => 'AID',
				aid  => 'r0Ot9FLF' . 'NAFxijLW'
			},
		},
	},
	NASA => {
		mgate       => 'https://reiseauskunft.insa.de/bin/mgate.exe',
		stopfinder  => 'https://reiseauskunft.insa.de/bin/ajax-getstop.exe',
		name        => 'Nahverkehrsservice Sachsen-Anhalt',
		productbits => [qw[ice ice regio regio regio tram bus ondemand]],
		request     => {
			client => {
				id   => 'NASA',
				v    => '4000200',
				type => 'IPH',
				name => 'nasaPROD',
				os   => 'iPhone OS 13.1.2',
			},
			ver  => '1.18',
			auth => {
				type => 'AID',
				aid  => 'nasa-' . 'apps',
			},
			lang => 'deu',
		},
	},
	NVV => {
		mgate      => 'https://auskunft.nvv.de/auskunft/bin/app/mgate.exe',
		stopfinder =>
		  'https://auskunft.nvv.de/auskunft/bin/jp/ajax-getstop.exe',
		name        => 'Nordhessischer VerkehrsVerbund',
		productbits =>
		  [qw[ice ic_ec regio s u tram bus bus ferry ondemand regio regio]],
		request => {
			client => {
				id   => 'NVV',
				v    => '5000300',
				type => 'IPH',
				name => 'NVVMobilPROD_APPSTORE',
				os   => 'iOS 13.1.2',
			},
			ext  => 'NVV.6.0',
			ver  => '1.18',
			auth => {
				type => 'AID',
				aid  => 'Kt8eNOH7' . 'qjVeSxNA',
			},
			lang => 'deu',
		},
	},
	'ÖBB' => {
		mgate       => 'https://fahrplan.oebb.at/bin/mgate.exe',
		stopfinder  => 'https://fahrplan.oebb.at/bin/ajax-getstop.exe',
		name        => 'Österreichische Bundesbahnen',
		productbits =>
		  [qw[ice ice ice regio regio s bus ferry u tram ice ondemand ice]],
		request => {
			client => {
				id   => 'OEBB',
				v    => '6030600',
				type => 'IPH',
				name => 'oebbPROD-ADHOC',
			},
			ver  => '1.41',
			auth => {
				type => 'AID',
				aid  => 'OWDL4fE4' . 'ixNiPBBm',
			},
			lang => 'deu',
		},
	},
	VBB => {
		mgate       => 'https://fahrinfo.vbb.de/bin/mgate.exe',
		stopfinder  => 'https://fahrinfo.vbb.de/bin/ajax-getstop.exe',
		name        => 'Verkehrsverbund Berlin-Brandenburg',
		productbits => [qw[s u tram bus ferry ice regio]],
		request     => {
			client => {
				id   => 'VBB',
				type => 'WEB',
				name => 'VBB WebApp',
				l    => 'vs_webapp_vbb',
			},
			ext  => 'VBB.1',
			ver  => '1.33',
			auth => {
				type => 'AID',
				aid  => 'hafas-vb' . 'b-webapp',
			},
			lang => 'deu',
		},
	},
	VBN => {
		mgate       => 'https://fahrplaner.vbn.de/bin/mgate.exe',
		stopfinder  => 'https://fahrplaner.vbn.de/hafas/ajax-getstop.exe',
		name        => 'Verkehrsverbund Bremen/Niedersachsen',
		productbits => [qw[ice ice regio regio s bus ferry u tram ondemand]],
		salt        => 'SP31mBu' . 'fSyCLmNxp',
		micmac      => 1,
		request     => {
			client => {
				id   => 'VBN',
				v    => '6000000',
				type => 'IPH',
				name => 'vbn',
			},
			ver  => '1.42',
			auth => {
				type => 'AID',
				aid  => 'kaoxIXLn' . '03zCr2KR',
			},
			lang => 'deu',
		},
	},
);

sub new {
	my ( $obj, %conf ) = @_;
	my $service = $conf{service};

	my %lwp_options = %{ $conf{lwp_options} // { timeout => 10 } };

	my $ua = LWP::UserAgent->new(%lwp_options);

	$ua->env_proxy;

	if ( not $conf{station} ) {
		confess('You need to specify a station');
	}

	if ( not defined $service and not defined $conf{url} ) {
		$service = $conf{service} = 'DB';
	}

	if ( defined $service and not exists $hafas_instance{$service} ) {
		confess("The service '$service' is not supported");
	}

	my $ref = {
		active_service => $service,
		arrivals       => $conf{arrivals},
		developer_mode => $conf{developer_mode},
		exclusive_mots => $conf{exclusive_mots},
		excluded_mots  => $conf{excluded_mots},
		messages       => [],
		results        => [],
		station        => $conf{station},
		ua             => $ua,
		now            => DateTime->now( time_zone => 'Europe/Berlin' ),
	};

	bless( $ref, $obj );

	if ( $hafas_instance{$service}{mgate} ) {
		return $ref->new_mgate(%conf);
	}
	return $ref->new_legacy(%conf);
}

sub new_mgate {
	my ( $self, %conf ) = @_;
	my $json    = JSON->new->utf8;
	my $service = $conf{service};

	my $now  = $self->{now};
	my $date = ( $conf{datetime} // $now )->strftime('%Y%m%d');
	my $time = ( $conf{datetime} // $now )->strftime('%H%M%S');

	my $lid;
	if ( $self->{station} =~ m{ ^ [0-9]+ $ }x ) {
		$lid = 'A=1@L=' . $self->{station} . '@';
	}
	else {
		$lid = 'A=1@O=' . $self->{station} . '@';
	}

	my $mot_mask = 2**@{ $hafas_instance{$service}{productbits} } - 1;

	my %mot_pos;
	for my $i ( 0 .. $#{ $hafas_instance{$service}{productbits} } ) {
		$mot_pos{ $hafas_instance{$service}{productbits}[$i] } = $i;
	}

	if ( my @mots = @{ $self->{exclusive_mots} // [] } ) {
		$mot_mask = 0;
		for my $mot (@mots) {
			$mot_mask |= 1 << $mot_pos{$mot};
		}
	}

	if ( my @mots = @{ $self->{excluded_mots} // [] } ) {
		for my $mot (@mots) {
			$mot_mask &= ~( 1 << $mot_pos{$mot} );
		}
	}

	my $req = {
		svcReqL => [
			{
				req => {
					type     => ( $conf{arrivals} ? 'ARR' : 'DEP' ),
					stbLoc   => { lid => $lid },
					dirLoc   => undef,
					maxJny   => 30,
					date     => $date,
					time     => $time,
					dur      => -1,
					jnyFltrL =>
					  [ { type => "PROD", mode => "INC", value => $mot_mask } ]
				},
				meth => 'StationBoard'
			}
		],
		%{ $hafas_instance{$service}{request} }
	};

	$req = $json->encode($req);
	$self->{post} = $req;

	my $url = $conf{url} // $hafas_instance{$service}{mgate};

	if ( my $salt = $hafas_instance{$service}{salt} ) {
		if ( $hafas_instance{$service}{micmac} ) {
			my $mic = md5_hex( $self->{post} );
			my $mac = md5_hex( $mic . $salt );
			$url .= "?mic=$mic&mac=$mac";
		}
		else {
			$url .= '?checksum=' . md5_hex( $self->{post} . $salt );
		}
	}

	if ( $conf{json} ) {
		$self->{raw_json} = $conf{json};
	}
	else {
		if ( $self->{developer_mode} ) {
			say "requesting $req from $url";
		}

		my $reply = $self->{ua}->post(
			$url,
			'Content-Type' => 'application/json',
			Content        => $self->{post}
		);
		if ( $reply->is_error ) {
			$self->{errstr} = $reply->status_line;
			return $self;
		}

		if ( $self->{developer_mode} ) {
			say decode( 'utf-8', $reply->content );
		}

		$self->{raw_json} = $json->decode( $reply->content );
	}

	$self->check_mgate;
	$self->parse_mgate;

	return $self;
}

sub new_legacy {
	my ( $self, %conf ) = @_;

	my $now     = $self->{now};
	my $date    = ( $conf{datetime} // $now )->strftime('%d.%m.%Y');
	my $time    = ( $conf{datetime} // $now )->strftime('%H:%M');
	my $mode    = $conf{arrivals} ? 'arr' : 'dep';
	my $lang    = 'd';
	my $service = $conf{service};

	$self->{post} = {
		input     => $conf{station},
		date      => $date,
		time      => $time,
		start     => 'yes',         # value doesn't matter, just needs to be set
		boardType => $mode,
		L         => 'vs_java3',
	};

	$self->set_productfilter;

	my $url = ( $conf{url} // $hafas_instance{$service}{url} ) . "/${lang}n";

	if ( $conf{xml} ) {

		# used for testing
		$self->{raw_xml} = $conf{xml};
	}
	else {
		if ( $self->{developer_mode} ) {
			say "requesting from $url";
		}
		my $reply = $self->{ua}->post( $url, $self->{post} );

		if ( $reply->is_error ) {
			$self->{errstr} = $reply->status_line;
			return $self;
		}

		$self->{raw_xml} = $reply->content;
	}

	# the interface often does not return valid XML (but it's close!)
	if ( substr( $self->{raw_xml}, 0, 5 ) ne '<?xml' ) {
		$self->{raw_xml}
		  = '<?xml version="1.0" encoding="iso-8859-15"?><wrap>'
		  . $self->{raw_xml}
		  . '</wrap>';
	}

	if ( defined $service and $service =~ m{ ^ VBB | NVV $ }x ) {

		# Returns invalid XML with tags inside HIMMessage's lead attribute.
		# Fix this.
		$self->{raw_xml}
		  =~ s{ lead = " \K ( [^"]+ ) }{ $1 =~ s{ < [^>]+ > }{}grx }egx;
	}

	# TODO the DB backend also retuns invalid XML (similar to above, but with
	# errors in delay="...") when setting the language to dutch/italian.
	# No, I don't know why.

	eval { $self->{tree} = XML::LibXML->load_xml( string => $self->{raw_xml} ) };

	if ( my $err = $@ ) {
		if ( $self->{developer_mode} ) {
			say $self->{raw_xml};
		}
		$self->{errstr} = "Backend returned invalid XML: $err";
		return $self;
	}

	if ( $self->{developer_mode} ) {
		say $self->{tree}->toString(1);
	}

	$self->check_input_error;
	$self->prepare_results;
	return $self;
}

sub set_productfilter {
	my ($self) = @_;

	my $service     = $self->{active_service};
	my $mot_default = '1';

	if ( not $service or not exists $hafas_instance{$service}{productbits} ) {
		return;
	}

	my %mot_pos;
	for my $i ( 0 .. $#{ $hafas_instance{$service}{productbits} } ) {
		$mot_pos{ $hafas_instance{$service}{productbits}[$i] } = $i;
	}

	if ( $self->{exclusive_mots} and @{ $self->{exclusive_mots} } ) {
		$mot_default = '0';
	}

	$self->{post}{productsFilter}
	  = $mot_default x ( scalar @{ $hafas_instance{$service}{productbits} } );

	if ( $self->{exclusive_mots} and @{ $self->{exclusive_mots} } ) {
		for my $mot ( @{ $self->{exclusive_mots} } ) {
			if ( exists $mot_pos{$mot} ) {
				substr( $self->{post}{productsFilter}, $mot_pos{$mot}, 1, '1' );
			}
		}
	}

	if ( $self->{excluded_mots} and @{ $self->{excluded_mots} } ) {
		for my $mot ( @{ $self->{excluded_mots} } ) {
			if ( exists $mot_pos{$mot} ) {
				substr( $self->{post}{productsFilter}, $mot_pos{$mot}, 1, '0' );
			}
		}
	}

	return;
}

sub check_input_error {
	my ($self) = @_;

	my $xp_err = XML::LibXML::XPathExpression->new('//Err');
	my $err    = ( $self->{tree}->findnodes($xp_err) )[0];

	if ($err) {
		$self->{errstr}
		  = $err->getAttribute('text')
		  . ' (code '
		  . $err->getAttribute('code') . ')';
		$self->{errcode} = $err->getAttribute('code');
	}

	return $self;
}

sub check_mgate {
	my ($self) = @_;

	if ( $self->{raw_json}{err} and $self->{raw_json}{err} ne 'OK' ) {
		$self->{errstr} = $self->{raw_json}{errTxt}
		  // 'error code is ' . $self->{raw_json}{err};
		$self->{errcode} = $self->{raw_json}{err};
	}
	elsif ( defined $self->{raw_json}{cInfo}{code}
		and $self->{raw_json}{cInfo}{code} ne 'OK'
		and $self->{raw_json}{cInfo}{code} ne 'VH' )
	{
		$self->{errstr}  = 'cInfo code is ' . $self->{raw_json}{cInfo}{code};
		$self->{errcode} = $self->{raw_json}{cInfo}{code};
	}
	elsif ( @{ $self->{raw_json}{svcResL} // [] } == 0 ) {
		$self->{errstr} = 'svcResL is empty';
	}
	elsif ( $self->{raw_json}{svcResL}[0]{err} ne 'OK' ) {
		$self->{errstr}
		  = 'svcResL[0].err is ' . $self->{raw_json}{svcResL}[0]{err};
		$self->{errcode} = $self->{raw_json}{svcResL}[0]{err};
	}

	return $self;
}

sub errcode {
	my ($self) = @_;

	return $self->{errcode};
}

sub errstr {
	my ($self) = @_;

	return $self->{errstr};
}

sub similar_stops {
	my ($self) = @_;

	my $service = $self->{active_service};

	if ( $service and exists $hafas_instance{$service}{stopfinder} ) {

		my $sf = Travel::Status::DE::HAFAS::StopFinder->new(
			url            => $hafas_instance{$service}{stopfinder},
			input          => $self->{station},
			ua             => $self->{ua},
			developer_mode => $self->{developer_mode},
		);
		if ( my $err = $sf->errstr ) {
			$self->{errstr} = $err;
			return;
		}
		return $sf->results;
	}
	return;
}

sub add_message {
	my ( $self, $json, $is_him ) = @_;

	my $short = $json->{txtS};
	my $text  = $json->{txtN};
	my $code  = $json->{code};
	my $prio  = $json->{prio};

	if ($is_him) {
		$short = $json->{head};
		$text  = $json->{text};
		$code  = $json->{hid};
	}

	# Some backends use remL for operator information. We don't want that.
	if ( $code eq 'OPERATOR' ) {
		return;
	}

	for my $message ( @{ $self->{messages} } ) {
		if ( $code eq $message->{code} and $text eq $message->{text} ) {
			$message->{ref_count}++;
			return $message;
		}
	}

	my $message = Travel::Status::DE::HAFAS::Message->new(
		short     => $short,
		text      => $text,
		code      => $code,
		prio      => $prio,
		ref_count => 1,
	);
	push( @{ $self->{messages} }, $message );
	return $message;
}

sub add_message_node {
	my ( $self, $node ) = @_;

	my $header = $node->getAttribute('header');
	my $lead   = $node->getAttribute('lead');

	for my $message ( @{ $self->{messages} } ) {
		if ( $header eq $message->{header} and $lead eq $message->{lead} ) {
			$message->{ref_count}++;
			return $message;
		}
	}
	my $message = Travel::Status::DE::HAFAS::Message->new(
		header    => $header,
		lead      => $lead,
		ref_count => 1,
	);
	push( @{ $self->{messages} }, $message );
	return $message;
}

sub messages {
	my ($self) = @_;
	return @{ $self->{messages} };
}

sub prepare_results {
	my ($self) = @_;
	my $mode = $self->{post}->{boardType};

	my $xp_element = XML::LibXML::XPathExpression->new('//Journey');
	my $xp_msg     = XML::LibXML::XPathExpression->new('./HIMMessage');

	if ( not defined $self->{tree} ) {
		return;
	}

	$self->{results} = [];

	$self->{strptime_obj} //= DateTime::Format::Strptime->new(
		pattern   => '%d.%m.%YT%H:%M',
		time_zone => 'Europe/Berlin',
	);

	for my $tr ( @{ $self->{tree}->findnodes($xp_element) } ) {

		my @message_nodes = $tr->findnodes($xp_msg);
		my $train         = $tr->getAttribute('prod');
		my $time          = $tr->getAttribute('fpTime');
		my $date          = $tr->getAttribute('fpDate');
		my $dest          = $tr->getAttribute('targetLoc');
		my $platform      = $tr->getAttribute('platform');
		my $new_platform  = $tr->getAttribute('newpl');
		my $delay         = $tr->getAttribute('delay');
		my $e_delay       = $tr->getAttribute('e_delay');
		my $info          = $tr->getAttribute('delayReason');
		my $operator      = $tr->getAttribute('operator');
		my @messages;

		if ( not( $time and $dest ) ) {
			next;
		}

		for my $n (@message_nodes) {
			push( @messages, $self->add_message_node($n) );
		}

		# Some backends report dd.mm.yy, some report dd.mm.yyyy
		# -> map all dates to dd.mm.yyyy
		if ( length($date) == 8 ) {
			substr( $date, 6, 0, '20' );
		}

		# TODO the first charactor of delayReason is special:
		# " " -> no additional data, rest (if any) is delay reason
		# else -> first word is not a delay reason but additional data,
		# for instance "Zusatzfahrt/Ersatzfahrt" for a replacement train
		if ( defined $info and $info eq q{ } ) {
			$info = undef;
		}
		elsif ( defined $info and substr( $info, 0, 1 ) eq q{ } ) {
			substr( $info, 0, 1, q{} );
		}

		$train =~ s{#.*$}{};

		my $datetime = $self->{strptime_obj}->parse_datetime("${date}T${time}");

		push(
			@{ $self->{results} },
			Travel::Status::DE::HAFAS::Result->new(
				sched_datetime => $datetime,
				datetime_now   => $self->{now},
				raw_delay      => $delay,
				raw_e_delay    => $e_delay,
				messages       => \@messages,
				train          => $train,
				operator       => $operator,
				route_end      => $dest,
				platform       => $platform,
				new_platform   => $new_platform,
				info           => $info,
			)
		);
	}
	return $self;
}

sub parse_mgate {
	my ($self) = @_;

	$self->{results} = [];

	if ( $self->{errstr} ) {
		return $self;
	}

	$self->{strptime_obj} //= DateTime::Format::Strptime->new(
		pattern   => '%Y%m%dT%H%M%S',
		time_zone => 'Europe/Berlin',
	);

	my @locL  = @{ $self->{raw_json}{svcResL}[0]{res}{common}{locL}  // [] };
	my @prodL = @{ $self->{raw_json}{svcResL}[0]{res}{common}{prodL} // [] };
	my @opL   = @{ $self->{raw_json}{svcResL}[0]{res}{common}{opL}   // [] };
	my @icoL  = @{ $self->{raw_json}{svcResL}[0]{res}{common}{icoL}  // [] };
	my @remL  = @{ $self->{raw_json}{svcResL}[0]{res}{common}{remL}  // [] };
	my @himL  = @{ $self->{raw_json}{svcResL}[0]{res}{common}{himL}  // [] };
	my @jnyL  = @{ $self->{raw_json}{svcResL}[0]{res}{jnyL}          // [] };

	for my $result (@jnyL) {
		my $date = $result->{date};
		my $time_s
		  = $result->{stbStop}{ $self->{arrivals} ? 'aTimeS' : 'dTimeS' };
		my $time_r
		  = $result->{stbStop}{ $self->{arrivals} ? 'aTimeR' : 'dTimeR' };
		my $datetime_s
		  = $self->{strptime_obj}->parse_datetime("${date}T${time_s}");
		my $datetime_r
		  = $time_r
		  ? $self->{strptime_obj}->parse_datetime("${date}T${time_r}")
		  : undef;
		my $delay
		  = $datetime_r
		  ? ( $datetime_r->epoch - $datetime_s->epoch ) / 60
		  : undef;

		my $destination  = $result->{dirTxt};
		my $is_cancelled = $result->{isCncl};
		my $jid          = $result->{jid};
		my $platform     = $result->{stbStop}{dPlatfS};
		my $new_platform = $result->{stbStop}{dPlatfR};

		my $product    = $prodL[ $result->{prodX} ];
		my $train      = $product->{prodCtx}{name};
		my $train_type = $product->{prodCtx}{catOutS};
		my $line_no    = $product->{prodCtx}{line};

		my $operator;
		if ( defined $product->{oprX} ) {
			if ( my $opref = $opL[ $product->{oprX} ] ) {
				$operator = $opref->{name};
			}
		}

		my @messages;
		for my $msg ( @{ $result->{msgL} // [] } ) {
			if ( $msg->{type} eq 'REM' and defined $msg->{remX} ) {
				push( @messages, $self->add_message( $remL[ $msg->{remX} ] ) );
			}
			elsif ( $msg->{type} eq 'HIM' and defined $msg->{himX} ) {
				push( @messages,
					$self->add_message( $himL[ $msg->{himX} ], 1 ) );
			}
			else {
				say "Unknown message type $msg->{type}";
			}
		}

		push(
			@{ $self->{results} },
			Travel::Status::DE::HAFAS::Result->new(
				sched_datetime => $datetime_s,
				rt_datetime    => $datetime_r,
				datetime       => $datetime_r // $datetime_s,
				datetime_now   => $self->{now},
				delay          => $delay,
				is_cancelled   => $is_cancelled,
				train          => $train,
				operator       => $operator,
				route_end      => $destination,
				platform       => $platform,
				new_platform   => $new_platform,
				messages       => \@messages,
			)
		);
	}
	return $self;
}

sub results {
	my ($self) = @_;
	return @{ $self->{results} };
}

# static
sub get_services {
	my @services;
	for my $service ( sort keys %hafas_instance ) {
		my %desc = %{ $hafas_instance{$service} };
		$desc{shortname} = $service;
		push( @services, \%desc );
	}
	return @services;
}

# static
sub get_service {
	my ($service) = @_;

	if ( defined $service and exists $hafas_instance{$service} ) {
		return $hafas_instance{$service};
	}
	return;
}

sub get_active_service {
	my ($self) = @_;

	if ( defined $self->{active_service} ) {
		return $hafas_instance{ $self->{active_service} };
	}
	return;
}

1;

__END__

=head1 NAME

Travel::Status::DE::HAFAS - Interface to HAFAS-based online arrival/departure
monitors

=head1 SYNOPSIS

	use Travel::Status::DE::HAFAS;

	my $status = Travel::Status::DE::HAFAS->new(
		station => 'Essen Hbf',
	);

	if (my $err = $status->errstr) {
		die("Request error: ${err}\n");
	}

	for my $departure ($status->results) {
		printf(
			"At %s: %s to %s from platform %s\n",
			$departure->time,
			$departure->line,
			$departure->destination,
			$departure->platform,
		);
	}

=head1 VERSION

version 3.01

=head1 DESCRIPTION

Travel::Status::DE::HAFAS is an interface to HAFAS-based
arrival/departure monitors, for instance the one available at
L<http://reiseauskunft.bahn.de/bin/bhftafel.exe/dn>.

It takes a station name and (optional) date and time and reports all arrivals
or departures at that station starting at the specified point in time (now if
unspecified).

=head1 METHODS

=over

=item my $status = Travel::Status::DE::HAFAS->new(I<%opts>)

Requests the departures/arrivals as specified by I<opts> and returns a new
Travel::Status::DE::HAFAS element with the results.  Dies if the wrong
I<opts> were passed.

Supported I<opts> are:

=over

=item B<station> => I<station>

The station or stop to report for, e.g.  "Essen HBf" or
"Alfredusbad, Essen (Ruhr)".  Mandatory.

=item B<datetime> => I<DateTime object>

Date and time to report for.  Defaults to now.

=item B<excluded_mots> => [I<mot1>, I<mot2>, ...]

By default, all modes of transport (trains, trams, buses etc.) are returned.
If this option is set, all modes appearing in I<mot1>, I<mot2>, ... will
be excluded. The supported modes depend on B<service>, use
B<get_services> or B<get_service> to get the supported values.

Note that this parameter does not work if the B<url> parameter is set.

=item B<exclusive_mots> => [I<mot1>, I<mot2>, ...]

If this option is set, only the modes of transport appearing in I<mot1>,
I<mot2>, ...  will be returned.  The supported modes depend on B<service>, use
B<get_services> or B<get_service> to get the supported values.

Note that this parameter does not work if the B<url> parameter is set.

=item B<language> => I<language>

Set language for additional information. Accepted arguments are B<d>eutsch,
B<e>nglish, B<i>talian and B<n> (dutch), depending on the used service.

=item B<lwp_options> => I<\%hashref>

Passed on to C<< LWP::UserAgent->new >>. Defaults to C<< { timeout => 10 } >>,
you can use an empty hashref to override it.

=item B<mode> => B<arr>|B<dep>

By default, Travel::Status::DE::HAFAS reports train departures
(B<dep>).  Set this to B<arr> to get arrivals instead.

=item B<service> => I<service>

Request results from I<service>, defaults to "DB".
See B<get_services> (and C<< hafas-m --list >>) for a list of supported
services.

=item B<url> => I<url>

Request results from I<url>, defaults to the one belonging to B<service>.

=back

=item $status->errcode

In case of an error in the HAFAS backend, returns the corresponding error code
as string. If no backend error occurred, returns undef.

=item $status->errstr

In case of an error in the HTTP request or HAFAS backend, returns a string
describing it.  If no error occurred, returns undef.

=item $status->results

Returns a list of arrivals/departures.  Each list element is a
Travel::Status::DE::HAFAS::Result(3pm) object.

If no matching results were found or the parser / http request failed, returns
undef.

=item $status->similar_stops

Returns a list of hashrefs describing stops whose name is similar to the one
requested in the constructor's B<station> parameter. Returns nothing if
the active service does not support this feature.
This is most useful if B<errcode> returns 'H730', which means that the
HAFAS backend could not identify the stop.

See Travel::Status::DE::HAFAS::StopFinder(3pm)'s B<results> method for details
on the return value.

=item $status->get_active_service

Returns a hashref describing the active service when a service is active and
nothing otherwise. The hashref contains the keys B<url> (URL to the station
board service), B<stopfinder> (URL to the stopfinder service, if supported),
B<name>, and B<productbits> (arrayref describing the supported modes of
transport, may contain duplicates).

=item Travel::Status::DE::HAFAS::get_services()

Returns an array containing all supported HAFAS services. Each element is a
hashref and contains all keys mentioned in B<get_active_service>.
It also contains a B<shortname> key, which is the service name used by
the constructor's B<service> parameter.

=item Travel::Status::DE::HAFAS::get_service(I<$service>)

Returns a hashref describing the service I<$service>. Returns nothing if
I<$service> is not supported. See B<get_active_service> for the hashref layout.

=back

=head1 DIAGNOSTICS

None.

=head1 DEPENDENCIES

=over

=item * Class::Accessor(3pm)

=item * DateTime(3pm)

=item * DateTime::Format::Strptime(3pm)

=item * LWP::UserAgent(3pm)

=item * XML::LibXML(3pm)

=back

=head1 BUGS AND LIMITATIONS

The non-default services (anything other than DB) are not well tested.

=head1 SEE ALSO

Travel::Status::DE::HAFAS::Result(3pm), Travel::Status::DE::HAFAS::StopFinder(3pm).

=head1 AUTHOR

Copyright (C) 2015-2020 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt>

=head1 LICENSE

This module is licensed under the same terms as Perl itself.