use strict;
use warnings;

package Term::Chrome;
# ABSTRACT: DSL for colors and other terminal chrome
our $VERSION = '2.01';

# Pre-declare packages
{
    package # no index: private package
        Term::Chrome::Color;
}


use Exporter 5.57 'import';  # perl 5.8.3
# @EXPORT is defined at the end

use Carp ();
use Scalar::Util ();
our @CARP_NOT = qw< Term::Chrome::Color >;

# Private constructor for Term::Chrome objects. Lexical, so cross-packages.
# Arguments:
# - class name
# - foreground color
# - background color
# - flags list
my $new = sub
{
    my ($class, @self) = @_;

    my $fg = $self[0];
    Carp::croak "invalid fg color $fg"
        if defined($fg) && ($fg < 0 || $fg > 255);
    my $bg = $self[1];
    Carp::croak "invalid bg color $bg"
        if defined($bg) && ($bg < 0 || $bg > 255);
    # TODO check flags

    bless \@self, $class
};


# Cache for color objects
my %COLOR_CACHE;

sub color ($)
{
    my $color = shift;
    die "invalid color" if ref $color;
    my $c = chr $color;
    # We can not use '$COLOR_CACHE{$c} ||= ...' because this requires overloading
    # We can not use 'no overloading' because this requires perl 5.10
    exists $COLOR_CACHE{$c}
    ?  $COLOR_CACHE{$c}
    : ($COLOR_CACHE{$c} = Term::Chrome::Color->$new($color, undef))
}


use overload
    '""' => 'term',
    '+'  => '_plus',
    '${}' => '_deref',
    '&{}' => '_chromizer',
    '.'   => '_concat',
    '!'   => '_reverse',
    'bool' => sub () { 1 },
    fallback => 0,
;

sub term
{
    my $self = shift;
    my ($fg, $bg) = @{$self}[0, 1];
    my $r = join(';', @{$self}[2 .. $#$self]);
    if (defined($fg) || defined($bg)) {
        $r .= ';' if @$self > 2;
        if (defined $fg) {
            # LeoNerd says that this should be ----------> "38:5:$fg"
            # according to the spec but gnome-terminal doesn't support that
            $r .= $fg < 8 ? (30+$fg) : $fg < 16 ? "9$fg" : "38;5;$fg";
            $r .= ';' if defined $bg;
        }
        #                                      -------> "48:5:$bg"
        $r .= $bg < 8 ? (40+$bg) : $bg < 16 ? "10$bg" : "48;5;$bg" if defined $bg;
    } else {
        return '' unless @$self > 2
    }
    "\e[${r}m"
}


sub _plus
{
    my ($self, $other, $swap) = @_;

    return $self unless defined $other;

    die 'invalid value for +' unless $other->isa(__PACKAGE__);

    my @new = @$self;
    $new[0] = $other->[0] if defined $other->[0];
    $new[1] = $other->[1] if defined $other->[1];
    push @new, @{$other}[2 .. $#$other];

    bless \@new
}

my %reverse = (
    # Unfortunately there isn't a perfect mapping
    # Reference:
    # https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf page 75
    1 => 22,
    2 => 22,
    3 => 23,
    4 => 24,   # Underlined
    5 => 25,
    6 => 25,
    7 => 27,
    8 => 28,
    9 => 29,
    21 => 24,  # Double underline

    22 => 1,
    23 => 3,
    24 => 4,
    25 => 5,
    27 => 7,
    28 => 8,
    29 => 9,
);

sub _reverse
{
    my $self = shift;
    my @new = (undef, undef);
    push @new, 39 if $self->[0]; # ResetFg
    push @new, 49 if $self->[1]; # ResetBg
    # Reset/ResetFlags/ResetFg/ResetBg are removed
    # Other flags are reversed
    push @new, map { (!$_ || $_ > 30 || !exists $reverse{$_}) ? () : $reverse{$_} } @{$self}[2..$#$self];
    bless \@new, 'Term::Chrome::Flag'
}

sub _deref
{
    \("$_[0]")
}

sub _concat
{
    $_[2] ? $_[1].$_[0]->term
          : $_[0]->term.$_[1]
}


sub _chromizer
{
    my $self = shift;
    my $begin = $self->term;
    my $end = $self->_reverse->term;
    sub {
        unless (defined $_[0]) {
            Carp::carp "missing argument in Term::Chrome chromizer";
            return
        }
        $begin . $_[0] . $end
    }
}

sub fg
{
    my $c = $_[0]->[0];
    defined($c) ? color($c) : undef
}

sub bg
{
    my $c = $_[0]->[1];
    defined($c) ? color($c) : undef
}

sub flags
{
    my $self = shift;
    return undef unless @$self > 2;
    __PACKAGE__->$new(undef, undef, @{$self}[2..$#$self])
}

package # no index: private package
    Term::Chrome::Color;

our @ISA = qw< Term::Chrome >;

use overload
    '/'   => '_over',
    # Even if overloading is set in the super class, we have to repeat it for old perls
    (
        $^V ge v5.18.0
        ? ()
        : (
            '""'  => \&Term::Chrome::term,
            '+'   => \&Term::Chrome::_plus,
            '${}' => \&Term::Chrome::_deref,
            '.'   => \&Term::Chrome::_concat,
            '!'   => \&Term::Chrome::_reverse,
            'bool' => sub () { 1 },
        )
    ),
    fallback => 0,
;

sub _over
{
    die 'invalid bg color for /' unless ref($_[1]) eq __PACKAGE__;
    Term::Chrome->$new($_[0]->[0], $_[1]->[0])
}

package # no index: private package
    Term::Chrome::Flag;

our @ISA = qw< Term::Chrome >;

use overload
    '+'   => '_plus',
    '!'   => '_reverse',
    # Even if overloading is set in the super class, we have to repeat it for old perls
    (
        $^V ge v5.18.0
        ? ()
        : (
            '""'  => \&Term::Chrome::term,
            '${}' => \&Term::Chrome::_deref,
            '.'   => \&Term::Chrome::_concat,
            'bool' => sub () { 1 },
        )
    ),
    fallback => 0,
;

sub _reverse
{
    my $self = shift;
    bless [
        undef, undef,
        # Reset/ResetFlags/ResetFg/ResetBg are removed
        map { (!$_ || $_ > 30 || !exists $reverse{$_}) ? () : $reverse{$_} } @{$self}[2..$#$self]
    ]
}

sub _plus
{
    my ($self, $other, $swap) = @_;

    return $self unless defined $other;

    Carp::croak(q{Can't combine Term::Chrome with }.$other)
        unless Scalar::Util::blessed $other;

    if ($other->isa(__PACKAGE__)) {
        # Reset
        return $other if !$other->[2];
        # ResetFlags
        return $other if $#$other == 8 || ($self->[2] && $self->[2] < 30 && $other->[2] == $reverse{$self->[2]});
        # Concat flags
        __PACKAGE__->$new(@$self, @{$other}[2..$#$other])
    } elsif ($other->isa(Term::Chrome::)) {
        $other->_plus($self, '')
    } else {
        Carp::croak(q{Can't combine Term::Chrome with }.ref($other))
    }
}


package
    Term::Chrome;

# Build the constants and the @EXPORT list
#
# This block must be after "use overload" (for both Term::Chrome
# and Term::Chrome::Color) because overload must be set before blessing
# due to a bug in perl < 5.18
# (according to a comment in Types::Serialiser source)

my $mk_flag = sub { Term::Chrome::Flag->$new(undef, undef, @_) };

my %const = (
    Reset      => $mk_flag->(''),
    ResetFg    => $mk_flag->(39),
    ResetBg    => $mk_flag->(49),
    ResetFlags => $mk_flag->(22, 23, 24, 25, 27, 28),
    Standout   => $mk_flag->(7),
    Underline  => $mk_flag->(4),
    Reverse    => $mk_flag->(7),
    Blink      => $mk_flag->(5),
    Bold       => $mk_flag->(1),

    Black      => color 0,
    Red        => color 1,
    Green      => color 2,
    Yellow     => color 3,
    Blue       => color 4,
    Magenta    => color 5,
    Cyan       => color 6,
    White      => color 7,

    # Larry Wall's favorite color
    # The true 'chartreuse' color from X11 colors is #7fff00
    # The xterm-256 color #118 is near: #87ff00
    Chartreuse => color 118,
);

our @EXPORT = ('color', keys %const);

# In 17fd029f we avoided to use constant.pm on perl < 5.16
# This does not seem necessary anymore.
require constant;
constant->import(\%const);

1;
# vim:set et ts=8 sw=4 sts=4:
