#!/usr/bin/ruby1.6 -Ke
# -*- ruby -*-

### mhc-sync -- Data synchronization tool for MHC.
##
## Author:  Yoshinari Nomura <nom@quickhack.net>
##
## Created: 2000/04/26
## Revised: 2000/10/30 16:43:25
##

## mhc-sync [-n] [-x exchange_id] [-r local_dir] [user@]remote.host[:dir]
##
##      -n             : Do nothing effectives. Useful for checking.
##      -x exchange_id : Set identical id for each pair of machines.
##      -r dir         : Set local repository directory of the MHC.
##                      (default: ~/Mail/schedule)
##
##      * You must install ssh and mhc-sync in both systems.
##      * Before you use it, you must make sure that the remote
##        repository and the local repository are identical.
##
##    for example on your note pc.
##       rsync -a --delete \
##         server_host:/home/someone/Mail/schedule/ /home/someone/Mail/schedule
##       (add or not add the trailing / is meaningful)
## TODO:
##      Much more Error Checking...
##      Repository is not unconfigurable yet.
##

## mhc-sync -s [-n] [-x exchange_id] [-r local_dir]
##    -- server mode. do not invoke by hand.

##
## global settings.
##

require 'mhc-kconv'
require 'mhc-date'
require 'mhc-schedule'

$DEBUG = false

STDOUT .sync = true
STDIN  .sync = true

$SSH           = 'ssh'
$CMD           = 'mhc-sync'
$DELIMITER     = "\x1\x1\x1\x1\n"
$DELIMITER2    = "\x1\x1\x1\x1\x1\x1\x1\x1\n"
$CONFLICT_INFO = "-- conflict --" * 5 + "\n"

##
## add some functionalities to MhcScheduleDB
##

class MhcScheduleItem
  def info
    ret = format("%s (first occurred %s)", subject, occur_min .to_s1('-'))
    return MhcKconv::todisp(ret)
  end
end

class MhcScheduleDB

  def modify_sch(sch)
    old_sch = get_record_by_id(sch .rec_id)
    del_sch(old_sch, false) if old_sch
    add_sch(sch)
  end
  
  def scan_all
    @record_id_to_path_hash = {}
    Dir .glob(@basedir + '/{[0-9]*/[0-9]*,intersect}/[0-9]*') .each{|filename|
      file = File .open(filename, 'r')
      header = file .gets("\n\n")
      file .close
      if header =~ /^X-SC-Record-Id:\s*(\S+)/i
	@record_id_to_path_hash[$1] = filename
	STDERR .print "#{filename} -> #{$1}\n" if $DEBUG
      end

    }
    return self
  end

  def get_record_by_id(record_id)
    if @record_id_to_path_hash[record_id]
      return MhcScheduleItem .new(@record_id_to_path_hash[record_id])
    else
      return nil
    end
  end
end

##
## option check
##

def usage
  print '
usage: mhc-sync [-n] [-x exchange_id] [-r local_dir] [user@]remote.host[:dir]

    -n             : Do nothing effectives. Useful for checking.
    -x exchange_id : Set identical id for each pair of machines.
    -r local_dir   : Set local repository directory of the MHC.
                     (default: ~/Mail/schedule)

    * You must install ssh and mhc-sync in both systems.
    * Before you use it, you must make sure that the remote
      repository and the local repository are identical.
'
  exit 1
end

$flag_noharm, $flag_syncid, $flag_serverp = false, nil, false
$flag_server_user, $flag_server_host, $flag_server_dir = nil, nil, nil
$flag_dir      = File .expand_path("~/Mail/schedule")
$flag_log_file = nil

while ARGV .length > 0
  case ARGV[0]
  when '-n'
    $flag_noharm = true
  when '-s'
    $flag_serverp = true
  when '-x'
    ARGV .shift
    $flag_syncid = ARGV[0]
  when '-r'
    ARGV .shift
    $flag_dir    = ARGV[0]
    usage() if ! File .directory?($flag_dir)
  when /^-/
    usage()
  else
    if ARGV[0] =~ /(([^@]+)@)?([^:\s]+)(:(.*))?/
      $flag_server_user, $flag_server_host, $flag_server_dir = $2, $3, $5
    else
      usage()
    end
  end
  ARGV .shift
end

$flag_log_file = $flag_dir + '/.mhc-db-log'

usage() if  $flag_serverp and  $flag_server_host
usage() if !$flag_serverp and !$flag_server_host

if $DEBUG
  STDERR .print "Serverp           = #{$flag_serverp .inspect}\n"
  STDERR .print "Dry run           = #{$flag_noharm .inspect}\n"
  STDERR .print "Sync id           = #{$flag_syncid .inspect}\n"
  STDERR .print "Remote user       = #{$flag_server_user .inspect}\n"
  STDERR .print "Remote host       = #{$flag_server_host .inspect}\n"
  STDERR .print "Remote repository = #{$flag_server_dir .inspect}\n"
  STDERR .print "Local repository  = #{$flag_dir .inspect}\n"
  STDERR .print "Local logfile     = #{$flag_log_file .inspect}\n"
end

##
##
##

if $flag_serverp
  STDERR .print "Initalizing remote (exchange_id=#{$flag_syncid}) ..."
else
  STDERR .print "Initalizing local ..."
end

db         = MhcScheduleDB .new($flag_dir).scan_all
log        = MhcLog .new($flag_log_file)
log_entry  = log .shrink_entries($flag_syncid)

STDERR .print " done.\n"

################
# server side
################

if $flag_serverp
  ## server sends: 
  ##
  ## M 2000-05-22 13:45:43 ... (1)
  ## M 2000-05-22 13:45:43 ... (2)
  ## DELIMITER
  ## contents of (1)
  ## DELIMITER
  ## contents of (2)
  ## :
  ## DELIMITER
  ## contents of (n)
  ## DELIMITER2

  log_entry .each{|e|
    print e .to_s, "\n"
  }

  log_entry .each{|e|
    if e .status == 'M' or e .status == 'A'
      print $DELIMITER
      sch = db .get_record_by_id(e .rec_id)
      if sch
	print sch .dump
      else
	STDERR .print "#{e .rec_id} missing\n"
      end
    end
  }
  print $DELIMITER2

  lines = ''
  got_delimiter = false
  while line = STDIN .gets
    if line =~ /^#{$DELIMITER2}/
      got_delimiter = true
      break 
    end
    lines += line
  end

  if got_delimiter
    commands = lines .split($DELIMITER)
    commands .each{|command|
      if command =~ /^DELETE\s+(\S+)/
	rec_id = $1
	sch = db .get_record_by_id(rec_id)
	if sch
	  STDERR .print "server: deleting #{sch .info}\n"
	  if !$flag_noharm
	    db .del_sch(sch) 
	  else
	    STDERR .print "S:deleting #{sch .info}\n"
	  end
	end
      elsif command =~ /^MODIFY/
	dummy, sch_string = command .split("\n", 2)
	sch = MhcScheduleItem .new(sch_string, false)
	if !$flag_noharm
	  db .modify_sch(sch)
	else
	  STDERR .print "S:modifiying #{sch .info}\n"
	  STDERR .print "*" * 70, "\n"
	  STDERR .print sch .dump
	  STDERR .print "*" * 70, "\n"
	end
      end
    }
    logent = MhcLogEntry .new('S', Time .now, $flag_syncid)
    if !$flag_noharm
      log .add_entry(logent)
    else
      STDERR .print "S:adding log entry #{logent}\n"
    end
  end

################
# client side
################
else

  STDERR .print"Connecting #{$flag_server_host} ...\n"
  svr = ($flag_server_user ?  $flag_server_user  + '@':'') + $flag_server_host
  dry = $flag_noharm ? '-n' : ''
  xid = $flag_syncid ? "-x #{$flag_syncid}" : ''
  dir = $flag_server_dir ? "-r #{$flag_server_dir}" : ''
  STDERR .print "#{$SSH} -x #{svr} #{$CMD} -s #{dry} #{xid} #{dir}\n" if $DEBUG
  inout = IO .popen("#{$SSH} -x #{svr} #{$CMD} -s #{dry} #{xid} #{dir}", "r+")
  inout .sync = true

  lines = ''
  svr_log_entry = {}
  svr_sch_entry = {}
  cli_log_entry = {}

  while line = inout .gets
    break if line =~ /^#{$DELIMITER2}/
    lines += line
  end

  log_lines, sch_lines = lines .split($DELIMITER, 2)

  if log_lines
    log_lines .split("\n") .each{|line|
      STDERR .print "LOGENTRY: #{line}\n" if $DEBUG
      ent =  MhcLogEntry .new(line)
      svr_log_entry[ent .rec_id] = ent
    }
  end

  if sch_lines
    sch_lines .split($DELIMITER) .each{|sch_article|
      sch = MhcScheduleItem .new(sch_article, false)
      svr_sch_entry[sch .rec_id] = sch
    }
  end
  ################
  log_entry .each{|e|
    cli_log_entry[e .rec_id] = e
  }
  ################################################################
  #
  #  D ... Deleted
  #  M ... Modified or Created
  #  - ... No entry or no change after last sync.
  #
  #                  Local
  #     +---+---------------------------------+
  #   R |   | D          M          -         |
  #   e +---+---------------------------------+
  #   m | D | --         CONFLICT   L DELETE  |
  #   o | M | CONFLICT   CONFLICT   R->L      |
  #   t | - | R DELETE   L->R       --        |
  #   e +---+---------------------------------+
  #
  #
  keys = (cli_log_entry .keys + svr_log_entry .keys) .uniq

  keys .each{|rec_id|
    l_val = cli_log_entry[rec_id]
    r_val = svr_log_entry[rec_id]

    l_status, r_status = '-', '-'

    r_status = r_val .status if r_val
    l_status = l_val .status if l_val

    r_status = 'M' if r_status == 'A'
    l_status = 'M' if l_status == 'A'

    lr_status = l_status + r_status

    r_sch = svr_sch_entry[rec_id]
    l_sch = db .get_record_by_id(rec_id)

    # STDERR .print "lr_status(#{rec_id}): #{lr_status}\n"
    case lr_status
    when 'DD' # peaceful
      ;;

    when 'DM' # conflict
      ## add the remote article to the local and inform it.
      STDERR .print "---------------------------------------------------\n"
      STDERR .print "You modified #{rec_id} : #{r_sch .info} in remote. "
      STDERR .print "But you deleted it in local at the same time.\n"
      STDERR .print "I tranferred it from remote to local again.\n"
      if !$flag_noharm
	db .add_sch(r_sch)
      else
	STDERR .print "C:adding #{r_sch .info}\n"
	STDERR .print "*" * 70, "\n"
	STDERR .print r_sch .dump
	STDERR .print "*" * 70, "\n"
      end
      
    when 'D-' # delete remote
      STDERR .print "---------------------------------------------------\n"
      STDERR .print "Delete remote #{rec_id}\n"
      inout .print "DELETE #{rec_id}\n"
      inout .print $DELIMITER

    when 'MD' # conflict
      ## add the remote article to the local and inform it.
      STDERR .print "---------------------------------------------------\n"
      STDERR .print "You modified #{rec_id} : #{l_sch .info} in local. "
      STDERR .print "But you deleted it in remote at the same time.\n"
      STDERR .print "I preserve the local one.\n"

    when 'MM' # conflict
      ## add the remote article to the local and inform it.
      STDERR .print "---------------------------------------------------\n"
      STDERR .print "You modified #{rec_id} : \n#{l_sch .info} in local.\n"
      STDERR .print "But you modified it in remote at the same time.\n"
      STDERR .print "I concatinate the remote one to the local one.\n"
      STDERR .print "Also, I preserved  the remote one.\n"
      STDERR .print "Please reedit the local one and do sync again.\n"
      l_sch .set_description(l_sch .description .to_s + $CONFLICT_INFO + r_sch .dump)
      if !$flag_noharm
	db .modify_sch(l_sch) 
      else
	STDERR .print "C:modifiying #{l_sch .info}\n"
	STDERR .print "*" * 70, "\n"
	STDERR .print l_sch .dump
	STDERR .print "*" * 70, "\n"
      end
	

    when 'M-' # transfer local to remote
      STDERR .print "---------------------------------------------------\n"
      STDERR .print "Transfer local to remote #{l_sch .info}\n"
      inout .print "MODIFY\n"
      inout .print "#{l_sch .dump}"
      inout .print $DELIMITER

    when '-D' # delete local
      if l_sch
	STDERR .print "---------------------------------------------------\n"
	STDERR .print "Delete local #{l_sch .info}\n"
	db .del_sch(l_sch) if !$flag_noharm
      end

    when '-M' # transfer remote to local
      STDERR .print "---------------------------------------------------\n"
      STDERR .print "Transfer  remote to local #{r_sch .info}\n"
      if !$flag_noharm
	db .modify_sch(r_sch) 
      else
	STDERR .print "C:modifiying #{r_sch .info}\n"
	STDERR .print "*" * 70, "\n"
	STDERR .print r_sch .dump
	STDERR .print "*" * 70, "\n"
      end

    when '--' # peaceful
      ;;
    end
  }
  inout .print $DELIMITER2
  logent = MhcLogEntry .new('S', Time .now, $flag_syncid)
  if !$flag_noharm
    log .add_entry(logent)
  else
    STDERR .print "C:adding log entry #{logent}\n"
  end
end

### Copyright Notice:

## Copyright (C) 1999, 2000 Yoshinari Nomura. All rights reserved.
## Copyright (C) 2000 MHC developing team. All rights reserved.

## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions
## are met:
## 
## 1. Redistributions of source code must retain the above copyright
##    notice, this list of conditions and the following disclaimer.
## 2. Redistributions in binary form must reproduce the above copyright
##    notice, this list of conditions and the following disclaimer in the
##    documentation and/or other materials provided with the distribution.
## 3. Neither the name of the team nor the names of its contributors
##    may be used to endorse or promote products derived from this software
##    without specific prior written permission.
## 
## THIS SOFTWARE IS PROVIDED BY Yoshinari Nomura AND CONTRIBUTORS ``AS IS''
## AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
## FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
## Yoshinari Nomura OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
## INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
## SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
## HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
## STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
## OF THE POSSIBILITY OF SUCH DAMAGE.

### mhc-sync ends here
