#!/usr/bin/perl -w

package CGI::Path;

use strict;
use vars qw($VERSION);

$VERSION = "1.02";

use CGI;

sub new {
  my $type  = shift;
  my %DEFAULT_ARGS = (
    allow_history         => 0,
    #magic_fill_hash      => {},
    form_name             => 'MYFORM',
    form_delete_pre_track => [],
    history_key           => 'history',
    htm_extension         => 'htm',
    keep_no_form_session  => 0,
    my_form               => {},
    my_path               => {},
    not_a_real_key        => [qw(_begin_time _printed_pages _session_id _validated)],
    path_hash             => {
#      simple example
#      initial_step       => 'page1',
#      page1              => 'page2',
#      page2              => 'page3',
#      page3              => '',
    },
    perl5lib              => $ENV{PERL5LIB} || '',
    session_only          => ['_validated'],
    session_wins          => [],
    use_session           => 1,
    val_extension         => 'val',
    validated_fresh       => {},
    WASA                  => [],
  );
  my $self = bless \%DEFAULT_ARGS, $type;

  $self->{my_module} ||= ref $self;
  $self->merge_in_args(@_);

  if($self->{use_session}) {
    $self->session;
  }

  ### don't always want to do all the extra stuff
  unless($self->{no_new_helper}) {
    $self->new_helper;
  }

  return $self;
}

sub session_dir {
  return '/tmp/path/session';
}

sub session_lock_dir {
  return '/tmp/path/session/lock';
}

sub cookies {
  my $self = shift;
  unless($self->{cookies}) {
    $self->{cookies} = {};
    my $query = CGI->new;
    foreach my $key ($query->cookie()) {
      $self->{cookies}{$key} = $query->cookie($key);
    }
  }
  return $self->{cookies};
}

sub DESTROY {
  my $self = shift;
}

sub session {
  my $self = shift;
  my $opt = shift;
  unless($self->{session}) {
    require Apache::Session::File;
    $self->{session} = {};
    tie %{$self->{session}}, 'Apache::Session::File', $self->sid, {
      Directory     => $self->session_dir,
      LockDirectory => $self->session_lock_dir,
    };
    $self->set_sid($self->{session}{_session_id});
  }
  if($opt) {
    my $opt_ref = ref $opt;
    if($opt_ref) {
      if($opt_ref eq 'HASH') {
        foreach(keys %{$opt}) {
          $self->{session}{$_} = $opt->{$_};
        }
      }
    } else {
      die "I got not a ref on session opt";
    }
  }
  return $self->{session};
}

sub sid_cookie_name {
  my $self = shift;
  return $self->my_content . "_sid";
}

sub set_cookie {
  my $self = shift;
  my ($cookie_name, $cookie_value) = @_;
  my $new_cookie = CGI::cookie
    (-name  => $cookie_name,
     -value => $cookie_value,
     );
  if (exists $ENV{CONTENT_TYPED}) {
    print qq{<meta http-equiv="Set-Cookie" content="$new_cookie">\n};
  } else {
    print "Set-Cookie: $new_cookie\n";
  }
  return;
}

sub set_sid {
  my $self = shift;
  my $sid = shift;
  $self->set_cookie($self->sid_cookie_name, $sid);
}

sub sid {
  my $self = shift;
  return $self->cookies->{$self->sid_cookie_name} || '';
}

sub merge_in_args {
  my $self = shift;
  my %PASSED_ARGS = (ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
  foreach my $passed_arg (keys %PASSED_ARGS) {
    if(ref $PASSED_ARGS{$passed_arg} && ref $PASSED_ARGS{$passed_arg} eq 'HASH') {
      foreach my $key (keys %{$PASSED_ARGS{$passed_arg}}) {
        $self->{$passed_arg}{$key} = $PASSED_ARGS{$passed_arg}{$key};
      }
    } else {
      $self->{$passed_arg} = $PASSED_ARGS{$passed_arg}
    }
  }
}

### morph methods

sub morph_path {
  my $self = shift;
  my $my_module = shift || $self->my_module;

  # morph to my_module
  if($my_module) {
    $self->morph($my_module, 1);
  }

}

sub morph_step {
  my $self = shift;

  my $step = shift;
  # going to morph based on my_module

  my $full_step = $self->my_module . "::$step";

  # morph to something like CGI::Path::Skel::page_one
  # the 1 turns on the -e check
  $self->morph($full_step, 1);
  
}

sub morph {
  my $self = shift;

  my $starting_ref = ref $self;

  my $package = shift;
  my $do_dash_e_check = shift;

  my $tmp_package = $package;
  $tmp_package =~ s@::@/@g;

  my $path = "$tmp_package.pm";

  my $exists = 1;

  # if they don't want to force the require, I will check -e before morphing
  if($do_dash_e_check) {
    my $full_path = "$self->{perl5lib}/$path";
    $exists = -e $full_path;
  }

  if($exists) {
    ### polymorph
    eval {
      require $path;
    };
    if( $@ ){
      $self->{errstr} = "bad stuff on require of $tmp_package.pm: $@";
      die $@;
    }
    bless $self, $package;
  }

  my $ending_ref = ref $self;

  if($self->can('add_WASA')) {
    $self->add_WASA($starting_ref);
    $self->add_WASA($ending_ref);
  }
  return $self;
}

sub add_WASA {
  my $self = shift;
  my $ref = shift;
  push @{$self->{WASA}}, $ref unless(grep { $_ eq $ref } @{$self->{WASA}});
}


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

sub base_include_path {
  my $self = shift;
  die "please write your own base_include_path method";
}

sub include_path {
  my $self = shift;
  return [$self->base_include_path . "/default"];
}

sub my_content {
  my $self = shift;
  return $self->{my_content} ||= do {
    my $my_content = lc($self->my_module);
    my $this_package = __PACKAGE__;
    $my_content =~ s/^${this_package}:://i;
    $my_content =~ s@::@/@g;
    $my_content; # return of the do
  };
}




sub new_helper {
  my $self = shift;

  if(!$self->{keep_no_form_session} && !scalar keys %{$self->this_form} && 
    scalar keys %{$self->session}) {
    #warn "User posted an empty form with a non empty session.\n";
    $self->session_wipe;
  }

  $self->generate_form;
  $self->morph_path;
  $self->get_path_array;

  unless($self->session->{_begin_time}) {
    $self->session({
      _begin_time => time,
    });
  }
  if($ENV{HTTP_REFERER} && $ENV{SCRIPT_NAME}
  && $ENV{HTTP_REFERER} !~ $ENV{SCRIPT_NAME}) {
    $self->session({
      http_referer => $ENV{HTTP_REFERER},
    });
  }
}

sub delete_session {
  my $self = shift;
  delete $self->{session};
}

sub session_wipe {
  my $self = shift;
  my $no_error = shift;
  $self->delete_cookie($self->sid_cookie_name);
  $self->delete_session;
  if(keys %{$self->this_form}) {
    die "need to get session_wipe to work generally";
  }
}

sub delete_cookie {
  my $self = shift;
  my $cookie_name = shift || die "need a cookie_name for delete_cookie";

  if($self->cookies->{$cookie_name}) {
    delete $self->cookies->{$cookie_name};
    $self->set_cookie($cookie_name, '');
  }
}

sub get_path_array {
  my $self = shift;

  my $path_hash = $self->path_hash;

  $self->{path_array} = [];
  my $next_step = $self->initial_step || die "need an initial_step";
  while($next_step) {
    die "infinite loop on $next_step" if(grep {$next_step eq $_ } @{$self->{path_array}});
    push @{$self->{path_array}}, $next_step;

    $next_step = $path_hash->{$next_step};
  }
  return $self->{path_array};
}

sub session_form {
  return {};
}

sub generate_form {
  # generate_form takes two hashes
  # $self->this_form - the results of CGI get form
  # $self->session   - the stuff from the session file
  # and merges them into
  # $self->{form} - the place to use
  my $self = shift;
  my $form = {};

  my $this_form = $self->this_form;
  # some things we want to just get from the session
  foreach(@{$self->{session_only}}) {
    delete $this_form->{$_};
    $form->{$_} = $self->session->{$_} if(exists $self->session->{$_});
  }

  # there might be some stuff we want to give session precedence to
  foreach(@{$self->{session_wins}}) {
    $form->{$_} = $self->session->{$_} if(exists $self->session->{$_});
  }

  # lay the hashes on top of each other in reverse order of precedence
  $self->{form} = {%{$self->session}, %{$this_form}, %{$form}};
  if($self->{form}{session_wipe}) {
    $self->session_wipe;
    $self->clear_value('session_wipe');
  }
}

sub this_form {
  my $self = shift;
  return $self->{this_form} ||= do {
    my $cgi = CGI->new;
    my %form = $cgi->Vars;
    \%form;
  }
}

sub form {
  my $self = shift;
  return $self->{form} || {};
}

sub allow_history {
  my $self = shift;
  my $return = 0;
  if($self->{allow_history} && $self->form->{$self->{history_key}}) {
    unless($self->session->{$self->{history_key}}) {
      $self->session({
        $self->{history_key} => $self->form->{$self->{history_key}},
      });
    }
    $return = 1;
  }
  return $return;
}

sub history_window_name {
  my $self = shift;
  return $self->my_content . "_window";
}

sub show_history {
  my $self = shift;
  return unless($self->allow_history);
  $self->content_type;
  my $window_name = $self->history_window_name;
  my $out = $self->out('history.tt', {
    history => $self->{history}
  });
  $$out =~ s@\n@\\n@g;
  $$out =~ s@(</?sc)(ript>)@$1" + "$2@ig;
  print <<SCRIPT;
<SCRIPT>
var w=window.open('', '$window_name', '');
w.document.open();
w.document.write("$$out");
w.document.close();
</SCRIPT>
SCRIPT
}

sub history_init {
  my $self = shift;
  if($self->allow_history) {
    $self->{history} = [];
  }
}

sub hook_history_init {
  my $self = shift;
  if($self->allow_history) {
    $self->{_history} ||= {};
    $self->{_history}{hook} = [];
  }
}

sub add_history_step {
  my $self = shift;
  if($self->allow_history) {
    my $step = shift || die "need a step";
    $self->{_history}{hash} = {};
    $self->{_history}{hash}{step} = $step;
  }
}

sub history_push {
  my $self = shift;
  if($self->allow_history) {
    push @{$self->{history}}, $self->{_history}{hash};
    delete $self->{_history};
  }
}

sub hook_history_add {
  my $self = shift;
  if($self->allow_history) {
    my $hash = shift || die "need a hook history hash";
    push @{$self->{_history}{hash}{hook}}, $hash;
  }
}

sub magic_fill_hash_interpolation_hash {
  return {
    localtime => scalar localtime,
    time      => time,
  }
}

sub navigate {
  my $self = shift;

  my $form = $self->form;
  my $path = $self->get_path_array;

  $self->history_init;

  $self->get_unvalidated_keys;
  $self->handle_jump_around;

  my $previous_step = $form->{_printed_pages} && $form->{_printed_pages}[-1] ? $form->{_printed_pages}[-1] : '';

  $self->can('pre_navigate_walk') && $self->pre_navigate_walk;
  
  ### foreach path, run the gamut of routines
  my $return_val = undef;
  foreach my $step (@$path){
    
    $self->add_history_step($step);

    return 1 if($self->{stop_navigate});
    $self->morph_step($step);

    $self->{this_step} = {
      this_step     => $step,
      previous_step => $previous_step,
      validate_ref  => $self->get_validate_ref($step),
    };
    
    my $method_pre  = "${step}_hook_pre";
    my $method_fill = "${step}_hash_fill";
    my $method_form = "${step}_hash_form";
    my $method_err  = "${step}_hash_errors";
    my $method_step = "${step}_step";
    my $method_post = "${step}_hook_post";

  # my $method_val  = "${step}_validate";
  #     method_val gets called in $self->validate

    ### a hook beforehand
    if( $self->can($method_pre) ){
      $return_val = $self->$method_pre();
      $self->hook_history_add({
        hook   => $method_pre,
        could  => 'Y',
        return => $return_val,
      });
      if($return_val) {

        $self->hook_history_add({
          hook   => $method_pre,
          could  => 'Y',
          return => $return_val,
        });
        $self->history_push;

        next;
      }
    } else {
      $self->hook_history_add({
        hook   => $method_pre,
        could  => 'N',
        return => undef,
      });
    }

    my $validated = 1;
    my $info_exists;

    if($self->info_exists($step)) {
      $info_exists = 1;

      $self->hook_history_add({
        hook   => 'info_exists',
        could  => 'Y',
        return => $info_exists,
      });

      $validated = $self->validate($step);

      $self->hook_history_add({
        hook   => 'validate',
        could  => 'Y',
        return => $validated,
      });

    } else {
      $info_exists = 0;

      $self->hook_history_add({
        hook   => 'info_exists',
        could  => 'Y',
        return => $info_exists,
      });

    }

    ### see if information is complete for this step
    if( ! $info_exists || ! $validated) {

      if($self->can($method_fill)) {
        my $fill_return = $self->add_to_fill($self->$method_fill);

        $self->hook_history_add({
          hook   => $method_fill,
          could  => 'Y',
          return => $fill_return,
        });

      } else {

        $self->hook_history_add({
          hook   => $method_fill,
          could  => 'N',
          return => undef,
        });

      }

      if(!($self->fill && keys %{$self->fill})) {
        $self->add_to_fill($self->form);

        $self->hook_history_add({
          hook   => 'add_to_fill',
          could  => 'Y',
          return => $self->form,
        });

      } else {

        $self->hook_history_add({
          hook   => 'add_to_fill',
          could  => 'N',
          return => undef,
        });

      }

      if($self->{magic_fill_hash}) {
        my $magic_fill_hash_interpolation_hash = $self->magic_fill_hash_interpolation_hash;
        my $add_to_fill = {};
        foreach my $key (keys %{$self->{magic_fill_hash}}) {
          next if(exists $self->fill->{$key});
          my $value = $self->{magic_fill_hash}{$key};
          if($value =~ /\[\%/) {
            my $tmp = '';
            $self->template->process(\$value, $magic_fill_hash_interpolation_hash, \$tmp);
            $value = $tmp;
          }
          $add_to_fill->{$key} = $value;
        }
        if(scalar keys %{$add_to_fill}) {
          $self->add_to_fill($add_to_fill);
        }
      }

      my $hash_form;
      if($self->can($method_form)) {
        $hash_form = $self->$method_form();

        $self->hook_history_add({
          hook   => $method_form,
          could  => 'Y',
          return => $hash_form,
        });

      } else {
        $hash_form = {};

        $self->hook_history_add({
          hook   => $method_form,
          could  => 'N',
          return => undef,
        });

      }

      my $hash_err;
      if($self->can($method_err)) {
        $hash_err = $self->$method_err();

        $self->hook_history_add({
          hook   => $method_err,
          could  => 'Y',
          return => $hash_err,
        });

      } else {
        $hash_err = {};

        $self->hook_history_add({
          hook   => $method_err,
          could  => 'N',
          return => undef,
        });

      }

      my $page_to_print;
      if($self->can($method_step)) {
        my $potential_page_to_print = $self->$method_step();

        # want to make this the page_to_print only if it a real page
        if($potential_page_to_print && !ref $potential_page_to_print && $potential_page_to_print !~ /^\d+$/) {
          $page_to_print = $potential_page_to_print 
        }

        $self->hook_history_add({
          hook   => $method_step,
          could  => 'Y',
          return => "$page_to_print ($potential_page_to_print)",
        });


      } else {

        $self->hook_history_add({
          hook   => $method_step,
          could  => 'N',
          return => undef,
        });

      }

      $page_to_print ||= $self->my_content . "/$step";

      my $val_ref = $self->{this_step}{validate_ref};
      $self->{my_form}{js_validation} ||= $self->generate_js_validation($val_ref);

      $self->hook_history_add({
        hook   => 'print',
        could  => 'Y',
        return => "printing $page_to_print",
      });
      $self->history_push;

      $self->print($page_to_print,
                   $hash_form,
                   $hash_err,
                   );
      return;
    }

    $self->history_push;

    ### a hook after
    if( $self->can($method_post) ){
      $return_val = $self->$method_post();
      if($return_val) {
        next;
      }
    }

  }
  return if $return_val;

  return $self->print($self->my_content . "/" . $self->initial_step ,$form);
}

sub generate_js_validation {
  my $self = shift;
  my $val_ref = shift || die "need a val_ref";
  my $form_name = $self->{form_name} || die "need a form name";
  require CGI::Ex::Validate;
  my $val = CGI::Ex::Validate->new();
  $CGI::Ex::Validate::JS_URI_PATH_VALIDATE = "/validate.js";
  $CGI::Ex::Validate::JS_URI_PATH_YAML = "/yaml_load.js";
  return $val->generate_js($val_ref, $form_name);
}

sub handle_jump_around {
  my $self = shift;

  warn "get handle_jump_around to work";
  return;
  my $path = $self->get_path_array;

  foreach my $step (reverse @{$path}) {
    if($self->fresh_form_info_exists($step)) {
      my $save_validated = delete $self->form->{_validated}{$step};

      foreach my $page_to_come (@{$self->pages_after_page($step)}) {

        if($self->page_has_displayed($page_to_come)) {
          my $cleared = 0;
          my $val_hash = $self->get_validate_ref($page_to_come);
          warn "get WipeOnBack to work";
          #foreach my $val_key (keys %{$val_hash}) {
          #  next unless($val_hash->{$val_key} && ref $val_hash->{$val_key} && ref $val_hash->{$val_key} eq 'HASH');
          #  if($val_hash->{$val_key}{WipeOnBack} && (! exists $self->this_form->{$val_key}) && exists $self->form->{$val_key}) {
          #    $self->clear_value($val_key);
          #    $cleared = 1;
          #  }
          #}

          if($cleared) {
            $save_validated .= delete $self->form->{_validated}{$page_to_come};
            ### need to make it look like these pages never got printed
            for(my $i=(scalar @{$self->form->{_printed_pages}}) - 1;$i>=0;$i--) {
              if($self->form->{_printed_pages}[$i] eq $page_to_come) {
                splice @{$self->form->{_printed_pages}}, $i, 1;
              }
            }
          }
        }
      }
      if($save_validated) {
        $self->save_value('_validated');
      }
    }
  }
}

sub pages_after_page {
  my $self = shift;
  my $step = shift;
  my $return = [];
  my $after = 0;
  foreach my $path_step (@{$self->get_path_array}) {
    push @{$return}, $path_step if($after);
    if($path_step eq $step) {
      $after = 1;
    }
  }
  return $return;
}

sub get_unvalidated_keys {
  my $self = shift;
  $self->{unvalidated_keys} = {%{$self->form}} || {};
  foreach(@{$self->{not_a_real_key}}) {
    delete $self->{unvalidated_keys}{$_};
  }
}

sub handle_unvalidated_keys {
  my $self = shift;
  warn "get handle_unvalidated_keys working again";
  return;
  my $path = $self->get_path_array;

  my $form = $self->form;

  my $validated = $form->{_validated} || {};
  my $mini_validated = {%$validated};

  foreach my $step (@$path){

    last unless(keys %{$self->{unvalidated_keys}});
    next if($mini_validated->{$step});

    my $val_hash = $self->get_validate_ref($step);

    my $to_save = {};
    foreach(keys %{$self->{unvalidated_keys}}) {
      if($self->{unvalidated_keys}{$_} && $form->{$_} && !$val_hash->{$_ . "_error"}) {
        $to_save->{$_} = $form->{$_};
      }
    }
    if(keys %$to_save) {
      $self->validate_unvalidated_keys($self->get_validate_ref($to_save));
      $self->session($to_save);
    }
  }
}

sub validate_unvalidated_keys {
  my $self = shift;
  my $validating_keys = shift;

  foreach(@{$validating_keys}) {
    delete $self->{unvalidated_keys}{$_};
  }
}

sub initial_step {
  my $self = shift;
  return $self->path_hash->{initial_step};
}

sub path_hash {
  my $self = shift;
  return $self->{path_hash} || die "need a hash ref for \$self->{path_hash}";
}

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

sub my_path_step {
  my $self = shift;
  my $step = shift;
  $self->my_path->{$step} ||= {};
  return $self->my_path->{$step};
}

sub get_validate_ref {
  my $self = shift;

  my $step = shift;
  my $return;
  my $step_hash = $self->my_path_step($step);
  if($step_hash && $step_hash->{validate_ref}) {
    $return = $step_hash->{validate_ref};
  } elsif($self->{validate_refs}) {

    ### can break out validate refs by content chunk
    if($self->{validate_refs}{$self->my_content} && $self->{validate_refs}{$self->my_content}{$step}) {
      $return = $self->{validate_refs}{$self->my_content}{$step};

    ### or just by step
    } elsif($self->{validate_refs}{$step}) {
      $return = $self->{validate_refs}{$step};
    }
  }
  unless($return) {
     $return = $self->include_validate_ref($self->my_content . "/$step");
  }
  $step_hash->{validate_ref} = $return;
  return $return;
}

sub include_validate_ref {
  my $self = shift;

  # step is the full step like path/skel/enter_info
  my $step = shift;

  my $val_filename = $self->get_full_path($self->step_with_extension($step, 'val'));
  return -e $val_filename ? $self->conf_read($val_filename) : {};
}

sub conf_read {
  my $self = shift;
  my $filename = shift;
  require YAML;
  my $ref;
  eval {
    $ref = YAML::LoadFile($filename);
  };
  if($@) {
    die "YAML error: $@";
  }
  return $ref;
}

sub page_name_helper {
  my $self = shift;
  my $base_page = shift || die "need a \$base_page for page_name_helper";
  $base_page = "content/$base_page" unless($base_page =~ m@^(content|images|template)/@);
  $base_page .= ".$self->{htm_extension}" unless($base_page =~ /\.\w+$/);
  return $base_page;
}

sub get_full_path {
  my $self = shift;
  my $relative_path = shift;
  $relative_path = $self->page_name_helper($relative_path);
  my $dirs = shift || $self->include_path;
  my $full_path = '';
  foreach my $dir (GET_VALUES($dirs)) {
    my $this_path = "$dir/$relative_path";
    if(-e $this_path) {
      $full_path = $this_path;
      last;
    }
  }
  return $full_path;
}

sub fresh_form_info_exists {
  my $self = shift;
  my $step = shift;
  my $return = 0;
  if($self->non_empty_val_ref($step) && $self->info_exists($step, $self->this_form)) {
    $return = 1;
  }
  return $return;
}

sub non_empty_val_ref {
  my $self = shift;
  my $step = shift;
  
  my $val_hash = $self->get_validate_ref($step);
  return $self->non_empty_ref($val_hash);
}

sub non_empty_ref {
  my $self = shift;
  my $ref = shift;
  my $non_empty = 0;
  if($ref) {
    my $ref_ref = ref $ref;
    if($ref_ref) {
      if($ref_ref eq 'HASH') {
        $non_empty = (scalar keys %{$ref}) ? 1 : 0;
      } elsif($ref_ref eq 'ARRAY') {
        $non_empty = (@{$ref}) ? 1 : 0;
      }
    }
  }
  return $non_empty;
}

sub info_exists {
  my $self = shift;
  my $step = shift;
  my $form = shift || $self->form;
  
  my $val_ref = $self->get_validate_ref($step);

  my $return = 0;
  #If the validate_ref default to true
  unless($self->non_empty_ref($val_ref)) {
    $return = 1;
  }
  
  my $validating_keys = $self->get_validating_keys($val_ref);
  #if there exists one key in the form that matches
  #one key in the validate_ref return true
  foreach(@{$validating_keys}) {
    if(exists $form->{$_}) {
      $return = 1;
    } 
  }
  return $return;
}

sub get_validating_keys {
  my $self = shift;
  my $val_ref = shift;
  require CGI::Ex::Validate;
  my $val = CGI::Ex::Validate->new;
  my $keys = $val->get_validation_keys($val_ref);
  return [sort keys %{$keys}];
}

sub page_has_displayed {
  my $self = shift;
  my $page = shift;
  return (grep /^$page$/, @{$self->form->{_printed_pages}});
}

sub page_was_just_printed {
  my $self = shift;
  my $page = shift;
  return (
    #Were we passed a page
    $page
     &&
    #Do we have any record of printed_pages
    ($self->form->{_printed_pages})
     &&
    #Is that record an array
    (ref $self->form->{_printed_pages} eq 'ARRAY')
     &&
    #Is there at least two items in this array
    ( scalar @{$self->form->{_printed_pages}})
     &&
    #Is the one before the current the page we were passed
    $self->form->{_printed_pages}[-1] eq $page
  );
}

sub validate {
  my $self = shift;
  my $validated = $self->form->{_validated} || {};

  my $this_step = $self->{this_step}{this_step};
  my $return = 1;

  my $show_errors = 1;
  if(!$self->page_was_just_printed($this_step)) {
    $show_errors = 0;
  }

  my $method_pre_val = "$self->{this_step}{this_step}_pre_validate";
  if($self->can($method_pre_val)) {
    $return = $self->$method_pre_val($show_errors) && $return;
  }

  if($validated->{$this_step}) {


  } else {

    if($self->validate_proper($self->form, $self->{this_step}{validate_ref})) {

      $return = 0;

    } else {
      $self->{validated_fresh}{$this_step} = 1;
      $validated->{$this_step} = 1;
      my $validated_hash = {
        _validated => $validated,
      };

      $self->validate_unvalidated_keys($self->get_validating_keys($self->{this_step}{validate_ref}));

      $self->form->{_validated} = $validated;
      # going to save the keys that have been validated to the session
      foreach my $key (@{$self->get_validating_keys($self->{this_step}{validate_ref})}) {
        $validated_hash->{$key} = $self->form->{$key};
      }
      $self->session($validated_hash);
    }
  }
  my $method_post_val = "$self->{this_step}{this_step}_post_validate";
  if($self->can($method_post_val)) {
    $return = $self->$method_post_val($show_errors) && $return;
  }
  if(!$return) {
    my $change = '';
    foreach my $check_page ($this_step, @{$self->pages_after_page($this_step)}) {
      $change .= (delete $validated->{$check_page}||'');
    }
    if($change) {
      $self->session({
        _validated => $validated,
      });
    }
  }
  return $return;
}

sub validate_proper {
  my $self = shift;
  my $form = shift;
  my $val_ref = shift;
  require CGI::Ex::Validate;
  my $errobj = CGI::Ex::Validate->new({
    as_hash_join   => "<BR>\n",
    required_error => '[% error.$field_required_error %]',
  })->validate($form, $val_ref);
  my $return = 0;
  if($errobj) {
    my $error_hash = $errobj->as_hash;
    $return = $self->add_my_error($error_hash);
  }
  return $return;
}

sub save_value {
  my $self = shift;
  my $name = shift;

  if (!ref $name) {
    $self->session({
      $name => $self->form->{$name}
    });
  } else {
    foreach my $key (keys %{$name}) {
      $self->form->{$key} = $name->{$key};
    }
    $self->session->save($name);
  }
}

sub clear_value {
  my $self = shift;
  my $name = shift;

  delete $self->form->{$name};
  delete $self->session->{form}{$name};
  delete $self->fill->{$name};
  $self->save_value($name => undef);
}

sub add_my_error {
  my $self = shift;
  my $errors = shift;

  unless(ref $errors && ref $errors eq 'HASH') {
    die "need to send a hash ref of errors" 
  }

  my $added = 0;
  $self->{my_form}{error} ||= {};

  foreach my $key (keys %{$errors}) {
    next unless($errors->{$key});
    $added++;
    $self->{my_form}{error}{$key} = $errors->{$key};
  }

  ### returns how many errors were added
  return $added;
}

sub fill {
  my $self = shift;
  $self->{fill} ||= {};
  return $self->{fill};
}

sub add_to_fill {
  my $self = shift;
  my $fill_to_add = shift;
  foreach(keys %{$fill_to_add}) {
    $self->fill->{$_} = $fill_to_add->{$_};
  }
}

sub out {
  my $self = shift;
  my $page = shift || die "need a page to \$self->out";
  my $form = shift || {};

  $page = $self->page_name_helper($page);
  my $out = $self->process($page, $form);
  $self->fill_in(\$out);
  return $out;
}

sub print {
  my $self = shift;
  my $step = shift;

  $self->handle_unvalidated_keys;


  my $out;

  if($self->{htm} && $self->{htm}{$step}) {
    my $content = $self->{htm}{$step};
    $self->template->process(\$content, $self->uber_form, \$out) || die $self->template->error;
    $self->fill_in(\$out);

  } elsif (!-e $self->get_full_path($self->step_with_extension($step, 'htm'))) {
    $out = $self->create_page($step);
    die "couldn't find content for page: $step" unless($out);
    $self->fill_in(\$out);
  }

  $self->record_page_print;
  $self->content_type;
  print $out ? $out : ${$self->out($step, $self->uber_form(\@_))};
}

sub fill_in {
  my $self = shift;
  my $content = shift;
  die "need a scalar ref for \$content" unless($content && ref $content && ref $content eq 'SCALAR');
  my $hashref = shift || $self->fill;
  require CGI::Ex;
  my $cgix = CGI::Ex->new;
  $cgix->fill({text => $content, form => $hashref});
}

sub uber_form {
  my $self = shift;
  my $others = shift || [];

  foreach my $hash (@{$others}) {
    next unless($hash && ref $hash && ref $hash eq 'HASH');
    foreach (keys %{$hash}) {
      next if(/^_/);
      $self->{uber_form}{$_} = $hash->{$_};
    }
  }

  $self->{uber_form} ||= {};
  $self->{uber_form}{fill} ||= {};
  foreach (keys %{$self->form}) {
    next if(/^_/);
    $self->{uber_form}{$_} = $self->form->{$_};
  }
  foreach (keys %{$self->{my_form}}) {
    $self->{uber_form}{$_} = $self->{my_form}->{$_};
  }
  foreach (keys %{$self->fill}) {
    next if(/^_/);
    $self->{uber_form}{fill}{$_} = $self->fill->{$_};
  }
  $self->{uber_form}{script_name} = $ENV{SCRIPT_NAME} || '';
  return $self->{uber_form};
}

sub process {
  my $self = shift;
  my $step_filename = shift || die "need a \$step_filename to \$self->process";
  my $form = shift || {};
  my $out = shift;

  unless(defined $out) {
    my $scalar = '';
    $out = \$scalar;
  }

  $self->template->process($step_filename, $form, $out) || die $self->template->error();
  #my $return = '';
  #$self->template->process($out, $form, \$return) || die $self->template->error();
  return ref $out ? $out : \$out;
}

sub step_with_extension {
  my $self = shift;
  my $step = shift;
  my $extension_type = shift;
  return "$step." . $self->{"${extension_type}_extension"};
}

sub template {
  require Template;
  my $self = shift;
  unless($self->{template}) {
    $self->{template} = Template->new({
      INCLUDE_PATH => $self->include_path,
    });
  }
  return $self->{template};
}

sub record_mail_print {
  my $self = shift;
  my $step = shift;
  my $printed_mail = $self->session->{printed_mail} || [];
  unless($step && $printed_mail->[-1] && $step eq $printed_mail->[-1]) {
    push @{$printed_mail}, $step;
    $self->session({
      printed_mail => $printed_mail,
    });
  }
}

sub record_page_print {
  my $self = shift;
  my $step = shift || $self->{this_step}{this_step};
  my $printed_pages = $self->session->{_printed_pages} || [];
  unless($step && $printed_pages->[-1] && $step eq $printed_pages->[-1]) {
    push @{$printed_pages}, $step;
    $self->session({
      _printed_pages => $printed_pages,
    });
  }
}

# This subroutine will generate a generic HTML page 
# with form fields for the required fields based on the validate file
sub create_page {
  my $self = shift;
  my $step = shift;

  my $form_name = $self->{form_name} || die "need a form name";

  $self->{create_page} ||= {};
  my $interpolate_hash = {
    full_step => $self->my_content . "/" . $self->{this_step}{this_step},
    form_name => $form_name,
  };
  $self->{create_page}{header} ||= <<HEADER;
<!-- this step nicely created: [% full_step %]-->
<HTML>
<HEAD>
<TITLE> created step: [% full_step %]</TITLE>
</HEAD>
<BODY>
HEADER

  my $validate_ref = $self->get_validate_ref($self->{this_step}{this_step});
  die "couldn't get validate_ref to create_page with" unless($validate_ref);

  $interpolate_hash->{validating_keys} = [];
  for my $name ( @{$self->get_validating_keys($validate_ref)}) {
    my $hash = {
      name => $name,
    };
    push @{$interpolate_hash->{validating_keys}}, $hash;
    #$content .= "[form.$name"."_required]";
    #$content .= "[|| form.$name"."_error env.blank]";
  }

  $self->{create_page}{js} ||= $self->generate_js_validation($validate_ref);
  $self->{create_page}{table_open} ||= "<TABLE>";
  $ENV{SCRIPT_NAME} ||= '';
  $ENV{PATH_INFO}   ||= '';
  $self->{create_page}{form_open} ||= "<FORM METHOD=post NAME='[% form_name %]' ACTION='$ENV{SCRIPT_NAME}$ENV{PATH_INFO}'>";
  $self->{create_page}{form} ||= <<FORM;
[% FOREACH hash = validating_keys %]
  <TR>
    <TD align=right>[% hash.name %]</TD>
    <TD><INPUT NAME='[% hash.name %]'></TD>
  </TR>
[% END %]
  <TR><TD><INPUT TYPE=submit NAME=next VALUE=next></TD></TR>
FORM
  $self->{create_page}{form_close} ||= "</FORM>";
  $self->{create_page}{table_close} ||= "</TABLE>";
  $self->{create_page}{footer} ||= <<FOOTER;
</BODY>
</HTML>
FOOTER

  my $content = <<CONTENT;
$self->{create_page}{header}
$self->{create_page}{form_open}
$self->{create_page}{table_open}
$self->{create_page}{form}
$self->{create_page}{table_close}
$self->{create_page}{form_close}
$self->{create_page}{footer}
$self->{create_page}{js}
CONTENT

  my $return = '';
  $self->template->process(\$content, $interpolate_hash, \$return);
  return $return;
}


sub GET_VALUES {
  my $values=shift;
  return () unless defined $values;
  if (ref $values eq "ARRAY") {
    return @$values;
  }
  return ($values);
}

sub URLEncode {
  my $arg = shift;
  my ($ref,$return) = ref($arg) ? ($arg,0) : (\$arg,1) ;

  if (ref($ref) ne 'SCALAR') {
    die "URLEncode can only modify a SCALAR ref!: ".ref($ref);
    return undef;
  }

  if ( (defined $$ref) && length $$ref) {
    $$ref =~ s/([^\w\.\-\ \@\/\:])/sprintf("%%%02X",ord($1))/eg;
    $$ref =~ y/\ /+/;
  }

  return $return ? $$ref : '';
}

sub content_type {
  unless($ENV{CONTENT_TYPED}) {
    print "Content-type: text/html\n\n";
    $ENV{CONTENT_TYPED} = 1;
  }
}

sub location_bounce {
  my $self = shift;
  my $url = shift;
  my $referer = shift;
  if (exists $ENV{CONTENT_TYPED}) {
    print "Location: <a href='$url'>$url</a><br>\n";
  } else {
    print "Status: 302\r\n";
    print "Referer: $referer\r\n" if($referer);
    print "Location: $url\r\n\r\n";
  }
  return 1;
}

1;

__END__

=head1 NAME

CGI::Path - module to aid in traversing one or more paths

=head1 SYNOPSIS

CGI::Path allows for easy navigation through a set of steps, a path.  It uses a session extensively (managed
by default via Apache::Session) to hopefully simplify path based cgis.

=head1 A PATH

A path is a package, like CGI::Path::Skel.  The path needs to be @ISA CGI::Path.  The package can contain
the step methods as described below.  You can also make a directory for the path, 
like CGI/Path/Skel, where the direectory will contain a package for each step.  This could be done from
your $ENV{PERL5LIB}.

=head1 path_hash

The path_hash is what helps generate the path_array, which is just an array of steps.  It is a hash to 
allow for easy overrides, since it is sort
of hard to override the third element of an array through a series of news.

The path_hash needs a key named 'initial_step', and then steps that point down the line, like so

  path_hash => {
    initial_step => 'page_one',
    page_one     => 'page_two',
    page_two     => 'page_three',
  },

since page_three doesn't point anywhere, the path_array ends.  You can just override $self->path_hash,
and have it return a hash ref as above.

It is quite easy to look at $ENV{PATH_INFO} and control multiple paths through a single cgi.  I offer the
following as a simple example

sub path_hash {
  my $self = shift;
  my $sub_path = '';
  if($ENV{PATH_INFO} && $ENV{PATH_INFO} =~ m@/(\w+)@) {
    $sub_path = $1;
  }
  my $sub_path_hash = {
    '' => {
      initial_step => 'main',
      main         => '',
    },
  };

  ### this is the generic path for adding something
  if($sub_path =~ /^add_(\w+)$/ && !exists $sub_path_hash->{$sub_path}) {
    $sub_path_hash->{$sub_path} = {
      initial_step          => $sub_path,
      $sub_path             => "${sub_path}_confirm",
      "${sub_path}_confirm" => "${sub_path}_receipt",
    };
  }
  $sub_path = '' unless(exists $sub_path_hash->{$sub_path});
  return $sub_path_hash->{$sub_path};
}

The above path_hash method was used to manage a series of distinct add paths.  Distinct paths added users,
categories, blogs and entries.  Each path was to handled differently, but they each had a path similar to the
add_user path, which looked like this

add_user => add_user_confirm => add_user_receipt

=head1 my_module

my_module by default is something like CGI::Path::Skel.  You can override $self->my_module and have it
return a scalar containing your my_module.  Module overrides are done based on my_module.

=head1 my_content

my_module by default is something like path/skel.  It defaults to a variant of my_module.  You can
override $self->my_content and have it return a scalar your my_content.  html content gets printed based
on my_content.

=head1 path_array

The path_array is formed from path_hash.  It is an array ref of the steps in the path.

=head1 navigate

$self->navigate walks through a path of steps, where each step corresponds to a .htm content
file and a .val validation hash.

A step corresponds to a .htm content file.  The .htm and .val need to share the base same name.

$self->{this_step} is hash ref containing the following
previous_step => the last step
this_step     => the current step
validate_ref  => the validation ref for the current step

Generally, navigate generates the form (see below), and for each step does the following

--  Get the validate ref (val_ref) for the given page
--  Comparing the val_ref to the form see if info exists for the step
--  Validate according to the val_ref
--  If validation fails, or if info doesn't exist, process the page and stop

More specifically, the following methods can be called for a step, in the given order.

step                    details/possible uses
---------------------------------------------
${step}_hook_pre        initializations, 
                        must return 0 or step gets skipped
info_exists             checks to see if you have info for this step
${step}_info_complete   can be used to make sure you have all the info you need

validate                contains the following
${step}_pre_validate    stuff to check before validate proper
validate_proper         runs the .val file validation
${step}_post_validate   stuff to run after validate proper

${step}_hash_fill       return a hash ref of things to add to $self->fill
                        fill is a hash ref of what fills the forms
${step}_hash_form       perhaps set stuff for $self->{my_form}
                        my_form is a hash ref that gets passed to the process method
${step}_hash_errors     set errors
${step}_step            do actual stuff for the step
${step}_hook_post       last chance

=head1 generate_form

The goal is that the programmer just look at $self->form for form or session information.  
To help facilitate this goal, I use the following

$self->this_form           - form from the current hit
$self->{session_only} = [] - things that get deleted from this_form and get inserted from the session
$self->{session_wins} = [] - this_form wins by default, set this if you want something just from the session

The code then sets the form with the following line

$self->{form} = {%{$self->session}, %{$this_form}, %{$form}};

=head1 Session management

CGI::Path uses Apache::Session::File by default for session management.  If you use this default you will need to write the following methods

session_dir      - returns the directory where the session files will go
session_lock_dir - returns the directory where the session lock files will go

=cut
