#                                                         -*- Perl -*-
# Copyright (c) 1999, 2000  Motoyuki Kasahara
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#

#
# 複数のファイルを連結するためのクラス。
#
package FreePWING::Link;

require 5.005;
require Exporter;
use English;
use FileHandle;
use FreePWING::Reference;
use strict;
use integer;

use vars qw(@ISA
	    @EXPORT
	    @EXPORT_OK
	    $block_length
	    $block_reference
	    $position_reference
	    $tag_reference);

@ISA = qw(Exporter);

#
# ブロックの長さ (バイト数)
#
$block_length = 2048;

#
# 参照情報の種別
#
$block_reference = 0;
$position_reference = 1;
$tag_reference = 2;

#
# 書式:
#	new()
# メソッドの区分:
# 	public クラスメソッド。
# 説明:
# 	新しいオブジェクトを作る。
# 戻り値:
# 	作成したオブジェクトへのリファレンスを返す。
#
sub new {
    my $type = shift;
    my $new = {
	# 出力ファイルのハンドラ
	'output_handle' => FileHandle->new(),

	# 出力ファイル名
	'output_file_name' => '',

	# 連結する各ファイルのオフセット
	'offset_table' => {},

	# 入力ファイルの 3 つ組
	# (入力ファイル、参照情報ファイル、タグファイル)
	'input_file_trios' => [],

	# エラーメッセージ
	'error_message' => '',
    };
    return bless($new, $type);
}

#
# 書式:
#	open(output_file_name)
#           output_file_name
#		出力ファイルの名前。
# メソッドの区分:
# 	public インスタンスメソッド。
# 説明:
# 	書き込み用に、結合ファイルを開く。
# 戻り値:
#	成功すれば 1 を返す。失敗すれば 0 を返す。
#
sub open {
    my $self = shift;
    my ($output_file_name) = @ARG;

    #
    # 結合ファイルを開く。
    #
    $self->{'output_file_name'} = $output_file_name;
    if (!$self->{'output_handle'}->open($self->{'output_file_name'}, 'w+')) {
	$self->{'error_message'} = 
	    "failed to open the file, $ERRNO: " . $self->{'output_file_name'};
	$self->close_internal();
	return 0;
    }
    binmode($self->{'output_handle'});

    return 1;
}

#
# 書式:
#	close()
# メソッドの区分:
# 	public インスタンスメソッド。
# 説明:
# 	参照情報、タグに従って結合ファイルを書き換えてから、結合ファ
#	イルを閉じる。結合ファイルを開いていなければ、何もしない。
# 戻り値:
#	成功すれば 1 を返す。失敗すれば 0 を返す。
#
sub close {
    my $self = shift;

    #
    # 結合ファイルを開いていなければ、メソッドを抜ける。
    #
    if (!$self->{'output_handle'}->fileno()) {
	return 1;
    }

    #
    # 結合ファイルの現在のサイズを得る。
    #
    my $output_file_length = $self->{'output_handle'}->tell();

    #
    # input_file_trios に登録された個々のタグファイルを処理する。
    #
    my $tag_table = {};
    my $file_trio;
    foreach $file_trio (@{$self->{'input_file_trios'}}) {
	my ($file_name, $reference_file_name, $tag_file_name) = @{$file_trio};

	if (!defined($tag_file_name) || $tag_file_name eq '') {
	    next;
	}

	#
	# タグファイルを開く。
	#
	my $handle = FileHandle->new();
	if (!$handle->open($tag_file_name, 'r')) {
	    $self->{'error_message'} = 
		"failed to open the file, $ERRNO: $tag_file_name";
	    $self->close_internal();
	    return 0;
	}

	#
	# 参照情報の各行を読む。
	#
	my $line;
	my @line_fields;
	for (;;) {
	    $line = $handle->getline();
	    if (!defined($line)) {
		last;
	    }
	    chomp $line;
	    @line_fields = split(/\t/, $line);
	    if (defined($tag_table->{$line_fields[0]})) {
		$self->{'error_message'} = 
		    "redefined tag, $line_fields[0]: line $NR, $tag_file_name";
		$self->close_internal();
		return 0;
	    }
	    $tag_table->{$line_fields[0]} = [hex($line_fields[1]), $file_name];
	}

	#
	# タグファイルを閉じる。
	#
	$handle->close();
    }

    #
    # input_file_trios に登録された参照情報ファイル毎に処理する。
    #
    my $file_trio;
    foreach $file_trio (@{$self->{'input_file_trios'}}) {
	my ($file_name, $reference_file_name, $tag_file_name) = @{$file_trio};

	if (!defined($reference_file_name) || $reference_file_name eq '') {
	    next;
	}

	#
	# 参照情報ファイルを開く。
	#
	my $handle = FileHandle->new();
	if (!$handle->open($reference_file_name, 'r')) {
	    $self->{'error_message'} = 
		"failed to open the file, $ERRNO: $reference_file_name";
	    $self->close_internal();
	    return 0;
	}

	#
	# 参照情報の各行を読む。
	#
	my $line;
	my @line_fields;
	my $buffer = '';
	my $max_buffer_size = $block_length * 32;
	my $buffer_position = 0;
	my $position_size;
	for (;;) {
	    $line = $handle->getline();
	    if (!defined($line)) {
		last;
	    }
	    chomp $line;
	    @line_fields = split(/\t/, $line);
	    
	    #
	    # 行の内容を確認。
	    #
	    my ($reference_type, $source_position, $target_position, 
		$target_file_name);
	    if ($line_fields[0] eq 'block') {
		$reference_type = $block_reference;
		$source_position = hex($line_fields[1]);
		$target_position = hex($line_fields[2]);
		$target_file_name = $line_fields[3];
	    } elsif ($line_fields[0] eq 'position') {
		$reference_type = $position_reference;
		$source_position = hex($line_fields[1]);
		$target_position = hex($line_fields[2]);
		$target_file_name = $line_fields[3];
	    } elsif ($line_fields[0] eq 'tag') {
		if (!defined($tag_table->{$line_fields[2]})) {
		    $self->{'error_message'} = "unknown tag name, $line_fields[2]: line $NR, $reference_file_name";
		    $self->close_internal();
		    return 0;
		}
		$reference_type = $tag_reference;
		$source_position = hex($line_fields[1]);
		$target_position = $tag_table->{$line_fields[2]}->[0];
		$target_file_name = $tag_table->{$line_fields[2]}->[1];
	    } else {
		$self->{'error_message'} = 
		    "invalid line: line $NR, $reference_file_name";
		$self->close_internal();
		return 0;
	    }

	    $position_size
		= ($reference_type == $block_reference) ? 4 : 6;

	    #
	    # 参照元と参照先の位置を算出する。
	    #
	    if (!defined($self->{'offset_table'}->{$target_file_name})) {
		$self->{'error_message'} = 
		    "unknown target file name, $target_file_name: line $NR, $reference_file_name";
		next;
	    }
	    $source_position += $self->{'offset_table'}->{$file_name};
	    $target_position += $self->{'offset_table'}->{$target_file_name};

	    #
	    # 結合ファイルの参照位置を書き換え。
	    #
	    if (($buffer_position + length($buffer) < $source_position)
		|| ($buffer_position > $source_position)) {
	        # 
	        # 書き込み位置に連続性がない場合はバッファを掃き出す。
	        #
 		if (length($buffer)) {
 		    if (!$self->{'output_handle'}
 			->seek($buffer_position, FileHandle->SEEK_SET)
 			|| !$self->{'output_handle'}->print($buffer)) {
 			$self->{'error_message'} = 
 			    "failed to seek or write the file, $ERRNO: "
		    . $self->{'output_file_name'};
		$handle->close();
		$self->close_internal();
		return 0;
	    }
 		    # $buffer_position += length($buffer);
 		    $buffer = '';
 		}
 		$buffer_position = $source_position;
	    }
	    
	    if ($buffer_position + length($buffer)
		   < $source_position + $position_size) {
		#
		# バッファへの読み込み。
		#
		my $tmp_buffer;

		if (!$self->{'output_handle'}
		    ->seek($buffer_position + length($buffer),
			   FileHandle->SEEK_SET)
		    || !$self->{'output_handle'}
		    ->read($tmp_buffer, $block_length)) {
		    $self->{'error_message'}
		    = "failed to seek or read the file, $ERRNO: "
			    . $self->{'output_file_name'};
		    $handle->close();
		    $self->close_internal();
		    return 0;
		}
		$buffer .= $tmp_buffer;
	    }

	    #
	    # 念の為エラーチェック。
	    #
	    if ($buffer_position + length($buffer)
		< $source_position + $position_size) {
		$self->{'error_message'}
		= "unexpected position.: $reference_file_name";
		    $handle->close();
		    $self->close_internal();
		    return 0;
		}

	    if ($reference_type == $block_reference) {
		substr($buffer, $source_position - $buffer_position, 4)
		    = pack('N', $target_position / $block_length + 1);
	    } elsif ($reference_type == $position_reference) {
		substr($buffer, $source_position - $buffer_position, 6)
		    = pack('Nn', $target_position / $block_length + 1,
			   $target_position % $block_length);
	    } else {
		my $target_block = bcd($target_position / $block_length + 1);
		my $target_offset = bcd($target_position % $block_length);
		substr($buffer, $source_position - $buffer_position, 6)
		    = pack('Nn', $target_block, $target_offset);
	    }

 	    if ($source_position + $position_size - $buffer_position
 		> $max_buffer_size) {
 		#
 		# 大きくなったバッファの掃き出し。
 		#
		if (!$self->{'output_handle'}
 		    ->seek($buffer_position, FileHandle->SEEK_SET)
 		    || !$self->{'output_handle'}
 		    ->print(substr($buffer, 0, $max_buffer_size))) {
		    $self->{'error_message'} = 
 			"failed to seek or write the file, $ERRNO: "
			    . $self->{'output_file_name'};
		    $handle->close();
		    $self->close_internal();
		    return 0;
		}
 		$buffer_position += $max_buffer_size;
 		substr($buffer, 0, $max_buffer_size) = '';
 	    }
	}

 	if (length($buffer)) {
 	    #
 	    # 終了処理としてバッファの掃き出し。
 	    # 
 	    if (!$self->{'output_handle'}
 		->seek($buffer_position, FileHandle->SEEK_SET)
 		|| !$self->{'output_handle'}
 		->print($buffer)) {
 		$self->{'error_message'} = 
 		    "failed to seek or write the file, $ERRNO: "
 		    . $self->{'output_file_name'};
 		$handle->close();
 		$self->close_internal();
 		return 0;
	    }
	}

	#
	# 参照情報ファイルを閉じる。
	#
	$handle->close();
    }

    #
    # 結合ファイルを閉じる。
    #
    $self->close_internal();

    return 1;
}

#
# $position の bcd 値を計算する。
#
sub bcd {
    my ($position) = shift;

    my $bcd = 0;
    $bcd += (($position)             % 10);
    $bcd += (($position / 10)        % 10) << 4;
    $bcd += (($position / 100)       % 10) << 8;
    $bcd += (($position / 1000)      % 10) << 12;
    $bcd += (($position / 10000)     % 10) << 16;
    $bcd += (($position / 100000)    % 10) << 20;
    $bcd += (($position / 1000000)   % 10) << 24;
    $bcd += (($position / 10000000)  % 10) << 28;
    $bcd += (($position / 100000000) % 10) << 32;

    return $bcd;
}

#
# 書式:
#	close_internal()
# メソッドの区分:
# 	private インスタンスメソッド。
# 説明:
#       close() の内部処理用メソッド。
#
sub close_internal {
    my $self = shift;

    if ($self->{'output_handle'}->fileno()) {
	$self->{'output_handle'}->close();
    }
}

#
# 書式:
#	add_file(file_name, [[reference_file_name, tag_file_name]])
#           file_name
#		リンクするファイルの名前。
#           reference_file_name
#		リンクするファイルに対応する参照情報ファイルの名前。
#           reference_file_name
#		リンクするファイルに対応するタグファイルの名前。
# メソッドの区分:
# 	public インスタンスメソッド。
# 説明:
# 	結合ファイルにファイルを一つ繋げる。
# 戻り値:
#	成功すれば 1 を返す。失敗すれば 0 を返す。
#
sub add_file {
    my $self = shift;
    my ($file_name, $reference_file_name, $tag_file_name) = @ARG;

    #
    # $file_name, $reference_file_name を input_file_trios に登録。
    #
    $self->{'offset_table'}->{$file_name} = $self->{'output_handle'}->tell();
    push(@{$self->{'input_file_trios'}},
	 [$file_name, $reference_file_name, $tag_file_name]);

    #
    # $file_name を開く。
    #
    my $handle = FileHandle->new();
    if (!$handle->open($file_name, 'r')) {
	$self->{'error_message'} =
	    "failed to open the file, $ERRNO: $file_name";
	$self->close_internal();
	return 0;
    }
    binmode($handle);

    #
    # $file_name を $self->{'output_file_name'} に追加。
    #
    my ($buffer, $input_bytes);
    for (;;) {
	$input_bytes = $handle->read($buffer, $block_length);
	if (!defined($input_bytes)) {
	    $self->{'error_message'} =
		"failed to read the file, $ERRNO: $file_name";
	    $handle->close();
	    $self->close_internal();
	    return 0;
	}
	if ($input_bytes == 0) {
	    last;
	}
	if (!$self->{'output_handle'}->print($buffer)) {
	    $self->{'error_message'} = "failed to write the file, $ERRNO: " 
		    . $self->{'output_file_name'};
	    $handle->close();
	    $self->close_internal();
	    return 0;
	}
    }

    #
    # $file_name を閉じる。
    #
    $handle->close();

    #
    # $self->{'output_file_name'} の、半端なブロックの後方を "\0" で
    # 埋める。
    #
    my $pad_length = $block_length
	- $self->{'output_handle'}->tell() % $block_length;
    if ($pad_length < $block_length
	&& !$self->{'output_handle'}->print("\0" x $pad_length)) {
	$self->{'error_message'} = "failed to write the file, $ERRNO: "
	    . $self->{'output_file_name'};
	$self->close_internal();
	return 0;
    }

    return 1;
}

######################################################################
# <インスタンス変数の値を返すメソッド群>
#
# 書式:
#	インスタンス変数名()
# メソッドの区分:
# 	public インスタンスメソッド。
# 戻り値:
#	インスタンス変数の値を返す。
#
sub file_name {
    my $self = shift;
    return $self->{'output_file_name'};
}

sub output_file_name {
    my $self = shift;
    return $self->{'output_file_name'};
}

sub error_message {
    my $self = shift;
    return $self->{'error_message'};
}

1;

