#                                                         -*- Perl -*-
# Copyright (c) 1999, 2000  Motoyuki Kasahara
# Copyright (c) 2008  Kazuhiro Ito
#
# 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::Hash;

require 5.005;
# require Exporter;
use English;
use FreePWING::Link;
# use FileHandle;
use strict;
use integer;

# use DB_File;
# use Fcntl;

use vars qw(@ISA
	    @EXPORT
	    @EXPORT_OK);

@ISA = qw(FreePWING::Link);

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

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

	# タグの情報を収納するファイル名
	'tag_file_name' => '',

	# タグの情報を収納するハッシュ
	'tag_table' => {},

	# 連結する各ファイルのオフセット
	'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'});

    $self->open_internal($output_file_name);

    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 $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($self->{'tag_table'}->{$line_fields[0]})) {
		$self->{'error_message'} = 
		    "redefined tag, $line_fields[0]: line $NR, $tag_file_name";
		$self->close_internal();
		return 0;
	    }
	    $self->{'tag_table'}->{$line_fields[0]} = $line_fields[1]."\t".$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($self->{'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, $target_file_name) = split(/\t/, $self->{'tag_table'}->{$line_fields[2]});
		$target_position = hex($target_position);
	    } 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;
}

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

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

    $self->close_internal2();
}

sub open_internal {
}

sub close_internal2 {
}


1;
