#!/usr/bin/env perl

# Copyright (c) 2026 snake_case_nemo <scnemo@sdf.org>
# SPDX-License-Identifier: BSD-2-Clause
#
# 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.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 THE COPYRIGHT HOLDER 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.

use strict;
use warnings;

# Options passed to the constructor are only used if they are not set in the
# config file.
# root_dir defaults to $HOME/gopher and the setting is commented out in the 
# example config that is automatically created.
my $geph = Geph::Map->new(
	root_dir	=> "$ENV{HOME}/gopher",
	logo_file	=> "logo",
	logo_color	=> "red",
	header_file	=> "header",
	static_file	=> "static",
	phlog_dir_names	=> "phlog,ansi_phlog",
	latest_files	=> 3
);

my $entries_hash = $geph->get_entry_list;
$geph->create_phlog_maps($entries_hash);
$geph->delete_main_gophermap;
$geph->write_logo;
$geph->write_header;
$geph->cow('think', 'bunny', $ARGV[0]) if $ARGV[0];
$geph->create_phlog_list($entries_hash);
$geph->write_static_file;
$geph->__chmod;

# insert date command for logging into phlog gophermaps
my @phlog_gms = (
    {
        gm          =>  $geph->{config}->{root_dir} . "/phlog/gophermap",
        log_file    =>  "phlog_access.log"
    },
    {
        gm          =>  $geph->{config}->{root_dir} . "/ansi_phlog/gophermap",
        log_file    =>  "ansi_phlog_access.log"
    }
);
for my $phlog_gm (@phlog_gms) {
    open P_GM, "<", $phlog_gm->{gm} or die $!;
    my @p_gm = <P_GM>;
    close P_GM;

    open NP_GM, ">", $phlog_gm->{gm} or die $!;
    print NP_GM "=date >> ../logs/" . $phlog_gm->{log_file} . "\n";
    print NP_GM $_ for @p_gm;
    close NP_GM;
}

################################################################################

=head1 NAME

Geph::Map - create gophermaps for my phlog

=head1 SYNOPSIS

	use Geph::Map;

	# Options passed to the constructor are only used if they are not set in the
	# config file.
	# root_dir defaults to $HOME/gopher and the setting is commented out in the 
	# example config that is automatically created.
	my $geph = Geph::Map->new(
	root_dir	=> "$ENV{HOME}/gopher",
	logo_file	=> "logo",
	logo_color	=> "red",
	header_file	=> "header",
	static_file	=> "static"
	);

	my $entries_hash = $geph->get_entry_list;
	$geph->create_phlog_maps($entries_hash);
	$geph->delete_main_gophermap;
	$geph->write_logo;
	$geph->write_header;
	$geph->create_phlog_list($entries_hash);
	$geph->write_static_file;

=cut

=head1 DESCRIPTION

	This is what I use.
	Creates main gophermap from static files containing gopher formatted
	text.
	Dynamically creates a list of latest entries of a phlog from text files.

=cut

package Geph::Map;

use strict;
use warnings;
use Time::Piece;
use File::Basename;
use File::Path qw(make_path);
use feature 'say';
use Data::Dumper;

use constant DEFAULT_CONFIG_LOCATION 	=> "$ENV{HOME}/.config/geph-map/config";
use constant POSSIBLE_CONFIG_LOCATIONS 	=> [(
	"$ENV{HOME}/.config/geph-map/config",
	"$ENV{HOME}/.geph-map/config",
	"$ENV{HOME}/.geph-map.conf",
)];

use constant COLOR => {
	RED		=> "\e[31m",
	GREEN		=> "\e[32m",
	YELLOW		=> "\e[33m",
	BLUE		=> "\e[34m",
	MAGENTA		=> "\e[35m",
	CYAN		=> "\e[36m",
	BOLD		=> "\e[1m",
	RESET		=> "\e[0m"
};

=head1 FUNCTIONS

=cut

sub new {
	my ($class, %args) = @_;
	# Get param or set default.
	my $root_dir		= delete $args{ root_dir } || "$ENV{HOME}/gopher";
	my $phlog_dir_names 	= delete $args{ phlog_dir_names } || "phlog";
	my $latest_files	= delete $args{ latest_files } || 3;
	my $examine_lines	= delete $args{ examine_lines } || 5;
	my $timestamp_fmt_in	= delete $args{ timestamp_fmt_in } || "%a %b %d %H:%M:%S %Z %Y";
	my $timestamp_fmt_out	= delete $args{ timestamp_fmt_out } || "[%d-%b-%Y]";
	my $timestamp_align	= delete $args{ timestamp_align } || "before";
	my $headline		= delete $args{ headline } || "====";
	my $header_file		= delete $args{ header_file } || "";
	my $logo_file		= delete $args{ logo_file } || "";
	my $static_file		= delete $args{ static_file } || "";
	my $logo_color		= delete $args{ logo_color } || "";
	my $txt_file_extension	= delete $args{ txt_file_extension } || "txt";
	# Get config location or use standard.
	my $try_config_location	= &{ sub {
		do { return $_ if -e $_; } for @{ &POSSIBLE_CONFIG_LOCATIONS } 
	} };
	my $_config_location = ($try_config_location) ? $try_config_location : DEFAULT_CONFIG_LOCATION;
	# Make config directory if it does not exist.
	my $_config_dir = dirname $_config_location;
	make_path($_config_dir) if ! -d $_config_dir;
	# default config
	my $_config = {
		root_dir		=> $root_dir,
		gm_file			=> "gophermap",
		etc_dots		=> "...",
		phlog_dir_names		=> $phlog_dir_names,
		latest_files		=> $latest_files,
		examine_lines		=> $examine_lines,
		timestamp_fmt_in	=> $timestamp_fmt_in,
		timestamp_fmt_out	=> $timestamp_fmt_out,
		timestamp_align		=> $timestamp_align,
		headline		=> $headline,
		header_file		=> $header_file,
		logo_file		=> $logo_file,
		static_file		=> $static_file,
		logo_color		=> $logo_color,
		txt_file_extension	=> $txt_file_extension
	};

	my $self = {
		config		=> $_config,
		config_dir	=> $_config_dir,
		config_location	=> $_config_location,
	};
	bless $self, $class;
	
	$self->_parse_config;

	return $self;
}

=head2 cow()

	$obj->cow('say', 'default', 'Moo!');
	$obj->cow('think', 'daemon', 'Moo?');

Insert text with cowsay or cowthink.

=cut

sub cow {
	my $self = shift;
	my $type = shift;
	my $cowfile = shift || "small";
	my $str = shift || "Moo"; 
	my $exec;
	if ($type eq "say") { $exec = "cowsay"; }
	elsif ($type eq "think") { $exec = "cowthink"; }
	else { return; }
	my $rd = $self->{config}->{root_dir};
	my $main_map = "$rd/gophermap";
	open GM, ">>", $main_map or die $!;
	print GM $_ for (`echo "$str" | $exec -f $cowfile`);
	print GM $_ for (`date`); say GM "";
	close GM;
}

=head2 __chmod()

	Fix access rights.

=cut

sub __chmod {
	my $self = shift;
	chmod 0644, "$self->{config}->{root_dir}/gophermap";
}

=head2 create_phlog_list

	TODO DESCRIPTION

=cut

sub create_phlog_list {
	my ($self, $entries) = @_;
	my $rd = $self->{config}->{root_dir};
	my $main_map = "$rd/gophermap";
	my $c;
	my $latest_files = $self->{config}->{latest_files};
	my $f_ext = $self->{config}->{txt_file_extension};
	open GM, ">>", $main_map or die $!;

	for my $dir ( keys %{ $entries } ) {
		my @entries = @{ $entries->{$dir} };
		print GM " " . $self->{config}->{headline} . " ";
		print GM $dir . " ";
		print GM $self->{config}->{headline} . "\n";

		$c = 0;
		while ($c < $latest_files) {
			last if ! $entries[$c];
			$entries[$c] =~s /(.*)\t(.*.txt)$/$1\t$dir\/$2/;
			say GM $entries[$c++];
		}
		# print MAP to phlog dir
		say GM "1...\t$dir\n";
	}

	close GM;
}

=head2 delete_main_gophermap

	Delete main gophermap. You probably want to do this. Other methods only
	append to main gophermap.

=cut

sub delete_main_gophermap {
	my $self = shift;
	my $rd = $self->{config}->{root_dir};
	my $main_map = "$rd/gophermap";
	return if ! -e $main_map;
	unlink $main_map;
}

sub _write_static_file {
	my ($self, $option) = @_;
	my $rd = $self->{config}->{root_dir};
	my $file = "$rd/$self->{config}->{$option}";
	return if ! -e $file;

	my $main_map = "$rd/gophermap";
	open F, "<", $file or die $!;
	open GM, ">>", $main_map or die $!;
	print GM $_ while <F>;
	close F;
	close GM;
}

=head2 write_logo()

	Prints the logo from log file if said file exists.
	Prints the logo in color if C<logo_color> is set.
	TODO specifiy details for configuration

=cut


sub write_logo {
	my $self = shift;
	my $rd = $self->{config}->{root_dir};
	my $lf = "$rd/$self->{config}->{logo_file}";
	return if ! -e $lf;
	
	# Logo Color
	my $lg = $self->{config}->{logo_color};
	$lg = ($lg) ? uc($lg) : "";

	my $main_map = "$rd/gophermap";
	open LF, "<", $lf or die $!;
	open GM, ">>", $main_map or die $!;
	say GM COLOR->{$lg} if COLOR->{$lg};
	print GM $_ while <LF>;
	say GM COLOR->{RESET} if COLOR->{$lg};
	close LF;
	close GM;
}

=head2 write_header()

	Print the header file if it exists.
	Uses the configuration option C<header_file>.

=cut

sub write_header {
	my $self = shift;
	my $rd = $self->{config}->{root_dir};
	$self->_write_static_file("header_file");
}

=head2 write_static_file

	Write file from option C<static_file> to main gophermap.

=cut

sub write_static_file {
	my $self = shift;
	my $rd = $self->{config}->{root_dir};
	$self->_write_static_file("static_file");
}


=head2 create_phlog_maps()

	Creates a gophermap file under each directory of C<phlog_dir_names>.

=cut

sub create_phlog_maps {
	my ($self, $entries) = @_;
	for my $dir ( keys %{ $entries } ) {
		my $gophermap = "$self->{config}->{root_dir}/$dir/gophermap";
		open GM, ">", $gophermap or die $!;
		say GM $_ for @{ $entries->{$dir} };
		close GM;
		chmod 0644, $gophermap;
	}
}


=head2 get_entry_list()

	Creates a list for entries in the phlog directories.

=cut

sub get_entry_list {
	my $self = shift;
	my $entries_hash = {};
	my @result;
	my @sorted;
	my @files;
	my $f_ext = $self->{config}->{txt_file_extension};

	for my $dir ((split /,/, $self->{ config }->{ phlog_dir_names })) {
		my $phlog_dir = "$self->{ config }->{ root_dir }/$dir";
		@result = ();
		@files = ();
		@sorted = ();
		@files = grep { /.*\.$f_ext$/ } glob("$phlog_dir/*");

		for (@files) {
			my ($timestamp, $date_string, $title, $filename) = $self->_get_file_info($_);
			$title =~ s/\.txt$//;
			$title =~ s/_/ /g;
			push @result, {
				timestamp => $timestamp,
				line	  => "0$date_string $title\t$filename"
			};
			chmod 0644, $_;
		}

		#  return result sorted
		push @sorted, $_->{line} for ( sort { $b->{timestamp} <=> $a->{timestamp} } @result) ;
		$entries_hash->{ $dir } = [ @sorted ];
	}

	return $entries_hash;
}

# TODO DESCRIPTION
sub _get_file_info {
	my $self = shift;
	my $f_path = shift or die;
	my $filename = basename($f_path);
	my $mtime = (stat $_)[9];
	my $t = gmtime($mtime);
	my $timestamp;
	my $tp;
	# my $date_string = $t->strftime("%a %b %d %H:%M:%S %Z %Y");
	my $date_string = $t->strftime("'$self->{config}->{timestamp_fmt_in}'");

	open my $fh, "<", $_ or die $!;
	my @first_four_lines = (<$fh>)[0..3];
	close $fh;

	my $title = (map { $_=~s/^!//;+$_; } grep { /^!.*/ } 
		@first_four_lines)[0] || $filename;
	chomp($title);
	$date_string = (grep { /\d{2}:\d{2}:?\d{2}/ } 
		@first_four_lines)[0] || $date_string;
	chomp($date_string);

	# UTC can make problems
	$date_string =~ s/\s+UTC//;
	my $format_in_without_utc = $self->{config}->{timestamp_fmt_in};
	$format_in_without_utc =~ s/%Z\s//g;

	eval { $tp = Time::Piece->strptime( $date_string, $format_in_without_utc); 1; };

	if ($@) { $timestamp = $mtime; }
	else { $timestamp = $tp->epoch; }

	$t = gmtime($timestamp);
	$date_string = $t->strftime($self->{config}->{timestamp_fmt_out});

	return ($timestamp, $date_string, $title, $filename);
}

# TODO DESCRIPTION
sub _parse_config {
	my ($self) = shift;

	if (! -e $self->{config_location}) {
		$self->_gen_example_config;
	}

	open CONFIG, "<", $self->{config_location} or die $!;
	my @config_file = <CONFIG>; close CONFIG;
	my $val;
	for my $k (keys %{ $self->{config} }) {
		$val = $self->_get_option(\@config_file, $k);
		$val =~ s/"//g if $val;
		# keep default if $val is undef
		$self->{config}->{ $k } = $val if $val;
	}
}

# TODO DESCRIPTION
sub _get_option {
	my ($self, $config_ref, $key) = @_;
	my $val = (grep { /^$key\s=\s.*$/ } @{ $config_ref })[0] || undef;
	return undef if !$val;
	$val = (split(/ = /, $val))[1];
	# trim leading, trailing spaces, newlines
	$val =~ s/^\s*(.*)\s*$/$1/;
	return $val;
}

# TODO DESCRIPTION
sub _gen_example_config {
	my $self = shift;
	my @example_config = <<END;
# vim: se ft=confini :
#
# The default for root_dir is gopher under the users' home directory.
# Environment variables are not recognized.
# root_dir = "/sdf/udd/s/scnemo/gopher"
#
# List of directories that are "phlogged", i.e. presented in a phlog style with
# timestamps and titles in your main gophermap.
# phlog_dir_names = "phlog" 
# latest_files = 3
#
# The first number of lines that will be searched to find a title and a time
# stamp.
# examine_lines = 5
# Time format read from your articles.
# If this format is found in the first examine_lines lines, it will be used.
# Falls back to the mtime of your file.
# Default ist the format as it is produced by the date command.
# timestamp_fmt_in = "%a %b %d %H:%M:%S %Z %Y"
#
# The format that will be shown before files.
# Default is [dd-Mmm-YYYY], e.g. [01-Jan-1970]
# timestamp_fmt_out = "[%d-%b-%Y]"
#
# Show the timestamp before or after the title.
# timestamp_align = before
# This is the string your headlines will be marked with
# headline = "===="
#
# The file to read your header from. Must be relative top root_dir.
# Leave empty for none.
# header_file = "your_header_file"
# The file to read your logo from. Must be relative to root_dir It will placed
# at the top of your main gophermap file. Leave empty to not display a logo.
# logo_file = ""
#
# Static content goes into this file
# static_file =
# Give your logo a little extra touch with some color.
# Mind that something like 31m will be displayed if the # client does not
# understand ansi color codes.
# You can choose between red, green, yellow, blue, magenta and cyan.
# logo_color =
#
# File extension to look for for text files.
# Everything else will be ignored.
# txt_file_extension = "txt"
END
	open EX_CFG, ">", $self->{config_location} or die $!;
	print EX_CFG $_ for @example_config;
	close EX_CFG;
}

=head1 CONFIGURATION FILE

Can be one of the folling:

    $HOME/.config/geph-map/config
    $HOME/.geph-map/config
    $HOME/.geph-map.conf

Default is

    $HOME/.config/geph-map/config

=head1 AUTHOR

snake_case_nemo E<lt>scnemo@sdf.orgE<gt>

=cut

1;
